版本:1.1.0b2 |发布日期:2016年7月1日

SQLAlchemy 1.1文档

会话基础

会话是做什么的?

从一般意义上说,Session会建立与数据库的所有对话,并代表您在其生命周期中加载或关联的所有对象的“保留区域”。It provides the entrypoint to acquire a Query object, which sends queries to the database using the Session object’s current database connection, populating result rows into objects that are then stored in the Session, inside a structure called the Identity Map - a data structure that maintains unique copies of each object, where “unique” means “only one object with a particular primary key”.

Session以基本无状态的形式开始。一旦查询被发出或者其他对象被持久化,它就从Engine请求连接资源,该连接资源与Session本身或映射的Table正在操作的对象。这个连接表示一个正在进行的事务,直到指示Session提交或回滚其挂起状态为止。

All changes to objects maintained by a Session are tracked - before the database is queried again or before the current transaction is committed, it flushes all pending changes to the database. 这被称为工作单元模式。

在使用Session时,注意与它相关的对象是由Session保存的事务的代理对象有很多事件会导致对象重新访问数据库以保持同步。虽然这种做法有其不足之处,但是可以从Session中“分离”对象并继续使用它们。通常情况下,当您想再次使用它们时,您会将分离的对象与另一个Session重新关联,以便它们可以恢复表示数据库状态的正常任务。

获得一个会话

Session is a regular Python class which can be directly instantiated. However, to standardize how sessions are configured and acquired, the sessionmaker class is normally used to create a top level Session configuration which can then be used throughout an application without the need to repeat the configurational arguments.

sessionmaker的用法如下所示:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# an Engine, which the Session will use for connection
# resources
some_engine = create_engine('postgresql://scott:tiger@localhost/')

# create a configured "Session" class
Session = sessionmaker(bind=some_engine)

# create a Session
session = Session()

# work with sess
myobject = MyObject('foo', 'bar')
session.add(myobject)
session.commit()

上面,sessionmaker调用为我们创建了一个工厂,我们将其分配给名为Session的名称。这个工厂被调用时,将使用我们给出的配置参数来创建一个新的Session对象。在这种情况下,通常情况下,我们已经配置工厂为连接资源指定一个特定的Engine

A typical setup will associate the sessionmaker with an Engine, so that each Session generated will use this Engine to acquire connection resources. 这个关联可以使用bind参数在上面的例子中设置。

在编写应用程序时,将sessionmaker工厂放在全局级别。然后这个工厂可以被其余的应用程序用作新的Session实例的来源,保持Session对象在一个地方被构造的配置。

sessionmaker工厂也可以与其他帮助器一起使用,这些帮助器由用户定义的sessionmaker传递,然后由帮助器维护。其中一些助手在When do I construct a Session, when do I commit it, and when do I close it?

将其他配置添加到现有的sessionmaker()

一个常见的情况是在模块导入时调用sessionmaker,但是与sessionmaker关联的一个或多个Engine还没有进行。在这个用例中,sessionmaker结构提供了sessionmaker.configure()方法,这个方法会将其他配置指令放到现有的sessionmaker当构造被调用时发生:

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine

# configure Session class with desired options
Session = sessionmaker()

# later, we create the engine
engine = create_engine('postgresql://...')

# associate it with our custom Session class
Session.configure(bind=engine)

# work with the session
session = Session()

使用替代参数创建特别会话对象

For the use case where an application needs to create a new Session with special arguments that deviate from what is normally used throughout the application, such as a Session that binds to an alternate source of connectivity, or a Session that should have other arguments such as expire_on_commit established differently from what most of the application wants, specific arguments can be passed to the sessionmaker factory’s sessionmaker.__call__() method. 这些参数将覆盖已经被放置的任何配置,比如下面的,针对特定的Connection构建新的Session

# at the module level, the global sessionmaker,
# bound to a specific Engine
Session = sessionmaker(bind=engine)

# later, some unit of code wants to create a
# Session that is bound to a specific Connection
conn = engine.connect()
session = Session(bind=conn)

Session与特定Connection关联的典型基本原理是维护外部事务的测试夹具 - 请参阅Joining a Session into an External Transaction (such as for test suites)为例。

会议常见问题

至此,许多用户已经对会话有疑问了。本节介绍了使用Session时遇到的最基本问题的迷你FAQ(请注意,我们也有一个real FAQ)。

什么时候做一个sessionmaker

只有一次,在您的应用程序的全球范围内的某个地方。它应该被看作是应用程序配置的一部分。如果您的应用程序在一个包中有三个.py文件,例如,您可以将sessionmaker行放在__init__.py文件中;从这一点上你的其他模块说“从mypackage导入会议”。That way, everyone else just uses Session(), and the configuration of that session is controlled by that central point.

If your application starts up, does imports, but does not know what database it’s going to be connecting to, you can bind the Session at the “class” level to the engine later on, using sessionmaker.configure().

在本节的例子中,我们经常会在正在调用Session的行的正上方创建sessionmaker但是这只是为了例子!实际上,sessionmaker将在模块级别上。实例化Session的调用将被放置在数据库对话开始的应用程序中。

什么时候构建一个Session,什么时候提交,何时关闭?

TL;博士;

  1. As a general rule, keep the lifecycle of the session separate and external from functions and objects that access and/or manipulate database data. 这将大大有助于实现可预测和一致的交易范围。
  2. 确保你对事务的开始和结束位置有一个清晰的概念,并保持事务short,这意味着它们以一系列操作结束,而不是无限期地开放。

一个Session通常是在可能预期数据库访问的逻辑操作开始时构建的。

每当用于与数据库对话时,Session在开始通信时立即开始数据库事务。假设autocommit标志保留为建议的False缺省值,则该事务将保持进行中,直至Session被回滚,提交或关闭。如果在前一个交易结束之后再次使用,那么Session将开始新的交易;由此可见,Session能够跨越多个交易有一个使用期限,但一次只能有一个。我们将这两个概念称为事务范围会话范围

这里的含义是,SQLAlchemy ORM鼓励开发人员在他们的应用程序中建立这两个范围,不仅包括范围的开始和结束,还包括范围的扩展,例如一个Session

开发人员确定这个范围的负担是SQLAlchemy ORM必须对如何使用数据库有强烈意见的一个领域。unit of work模式具体是随着时间的推移而累积的变化,并周期性地刷新它们,保持内存状态与已知在本地事务中出现的状态同步。这种模式只有在有意义的事务范围到位时才有效。

虽然各种各样的应用程序体系结构可能会引入具有挑战性的情况,但确定开始和结束Session范围的最佳点通常并不难。

一个常见的选择是在交易结束的同时拆除Session,这意味着交易和会话范围是相同的。这是一个很好的选择,因为它消除了将会话范围视为与事务范围分开的需要。

虽然关于如何确定交易范围并没有一成不变的建议,但还是有一些共同的模式。特别是如果你正在编写一个Web应用程序,选择已经非常成熟。

Web应用程序是最简单的情况,因为这样的应用程序已经围绕一个一致的范围构建 - 这是请求,它表示来自浏览器的传入请求,处理该请求以制定回应,最后把这个回应传递给客户。然后,将Web应用程序与Session集成是将Session的作用域与请求的作用域链接在一起的简单任务。Session可以在请求开始的时候建立,也可以使用懒惰初始化模式,只要需要就建立一个模式。然后,请求继续进行,在某个系统中,应用程序逻辑可以以与访问实际请求对象的方式相关的方式访问当前的Session当请求结束时,Session也会被拆除,通常是通过使用Web框架提供的事件挂钩。The transaction used by the Session may also be committed at this point, or alternatively the application may opt for an explicit commit pattern, only committing for those requests where one is warranted, but still always tearing down the Session unconditionally at the end.

一些网页框架包括基础设施,以协助将Session的生命周期与web请求的寿命对齐。这包括用于与Flask Web框架结合使用的Flask-SQLAlchemy和通常与Pyramid框架一起使用的Zope-SQLAlchemySQLAlchemy建议将这些产品用作可用的。

In those situations where the integration libraries are not provided or are insufficient, SQLAlchemy includes its own “helper” class known as scoped_session. 关于这个对象的使用教程在Contextual/Thread-local Sessions它提供了将Session与当前线程相关联的快速方法,以及将Session对象与其他类型的范围相关联的模式。

如前所述,对于非Web应用程序,没有一个清晰的模式,因为应用程序本身不具有一种架构模式。最好的策略是试图划定“操作”,特定线程开始在一段时间内执行一系列操作的点,这些操作可以在最后被执行。一些例子:

  • 产生子分叉的后台守护进程会希望为每个子进程创建一个Session,并通过该分叉的“作业”生命周期来处理Session处理,然后在作业完成后撕下来。
  • 对于命令行脚本,应用程序将创建一个在程序开始工作时建立的单个全局Session,并在程序完成任务时提交。
  • 对于GUI界面驱动的应用程序来说,Session的范围可能最好在用户生成事件的范围内,例如按钮按下。或者,范围可以对应于明确的用户交互,诸如用户“打开”一系列记录,然后“保存”它们。

作为一般规则,应用程序应该管理会话外部的生命周期,以处理特定数据的函数。这是一个根本分离的问题,它使得特定于数据的操作不受其访问和操作数据的上下文的影响。

例如。 不要这样做

### this is the **wrong way to do it** ###

class ThingOne(object):
    def go(self):
        session = Session()
        try:
            session.query(FooBar).update({"x": 5})
            session.commit()
        except:
            session.rollback()
            raise

class ThingTwo(object):
    def go(self):
        session = Session()
        try:
            session.query(Widget).update({"q": 18})
            session.commit()
        except:
            session.rollback()
            raise

def run_my_program():
    ThingOne().go()
    ThingTwo().go()

保持会话(通常是交易)的生命周期独立和外部

### this is a **better** (but not the only) way to do it ###

class ThingOne(object):
    def go(self, session):
        session.query(FooBar).update({"x": 5})

class ThingTwo(object):
    def go(self, session):
        session.query(Widget).update({"q": 18})

def run_my_program():
    session = Session()
    try:
        ThingOne().go(session)
        ThingTwo().go(session)

        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()

高级开发人员会尽量保持会话,事务和异常管理的细节,尽可能避免程序工作的细节。例如,我们可以使用上下文管理器进一步分离关注点:

### another way (but again *not the only way*) to do it ###

from contextlib import contextmanager

@contextmanager
def session_scope():
    """Provide a transactional scope around a series of operations."""
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()


def run_my_program():
    with session_scope() as session:
        ThingOne().go(session)
        ThingTwo().go(session)

Session是缓存吗?

Yeee ...没有。它有点用作缓存,因为它实现了identity map模式,并存储了键入其主键的对象。但是,它不会执行任何类型的查询缓存。这意味着,如果你说session.query(Foo).filter_by(name='bar'),即使Foo(name='bar')在身份地图中,会议不知道这一点。它必须向数据库发出SQL,取回行,然后当它看到行中的主键时,它可以查看本地标识映射,看到对象已经存在。只有当你说query.get({some primary key)) Session

此外,Session会默认使用弱引用来存储对象实例。这也违背了使用Session作为缓存的目的。

Session不是一个全局对象,每个人都可以作为对象的“注册表”进行咨询。这更像是一个二级缓存的工作。SQLAlchemy使用dogpile.cache,通过Dogpile Caching示例提供了实现二级缓存的模式。

如何获得某个对象的Session

使用Session上的object_session() classmethod:

session = Session.object_session(someobject)

更新的Runtime Inspection API系统也可以使用:

from sqlalchemy import inspect
session = inspect(someobject).session

会话线程安全吗?

Session非常适用于非并发方式,这通常意味着一次只有一个线程。

应该以这样的方式使用Session,即单个事务中的单个操作系列存在一个实例。获得这种效果的一个便捷方法是通过将Session与当前线程相关联(请参阅Contextual/Thread-local Sessions)。另一种是使用Session在函数之间传递的模式,否则不与其他线程共享。

更重要的一点是,你不应该使用多个并发线程的会话。这就好像让一家餐厅的每个人都从同一个盘子里吃东西一样。会话是一个本地的“工作空间”,您用于一组特定的任务;您不希望或需要与其他正在执行其他任务的线程共享该会话。

确保Session一次只在一个并发线程中使用,称为“无共享”方法来实现并发。但实际上,不共享Session意味着更重要的模式;它不仅意味着Session对象本身,而且与该会话关联的所有对象都必须保持在单个并发线程的范围内。Session关联的一组映射对象本质上是代理数据库行中访问的数据库中的数据,所以就像Session本身一样,整个对象实际上只是数据库连接(或连接)的大规模代理。最终,它主要是我们远离并发访问的DBAPI连接本身;但是由于Session和与其关联的所有对象都是该DBAPI连接的代理,所以整个图对于并发访问本质上是不安全的。

If there are in fact multiple threads participating in the same task, then you may consider sharing the session and its objects between those threads; however, in this extremely unusual scenario the application would need to ensure that a proper locking scheme is implemented so that there isn’t concurrent access to the Session or its state. 对这种情况更常见的做法是每个并发线程维护一个Session,而不是从一个Session复制对象到另一个使用Session.merge()方法将对象的状态复制到不同的Session本地的新对象中。

使用会话的基础

这里介绍最基本的Session使用模式。

查询¶ T0>

query()函数接受一个或多个实体,并返回一个新的Query对象,该对象将在本会话的上下文中发出映射器查询。一个实体被定义为映射类,Mapper对象,启用orm的描述符AliasedClass对象:

# query from a class
session.query(User).filter_by(name='ed').all()

# query with multiple classes, returns tuples
session.query(User, Address).join('addresses').filter_by(name='ed').all()

# query using orm-enabled descriptors
session.query(User.name, User.fullname).all()

# query from a mapper
user_mapper = class_mapper(User)
session.query(user_mapper)

Query返回结果时,实例化的每个对象都存储在标识映射中。当一行匹配已经存在的对象时,返回相同的对象。在后一种情况下,行是否被填充到现有对象上取决于实例的属性是否已被过期默认配置的Session会自动使所有实例沿着事务边界过期,所以对于通常是孤立的事务,不应该存在表示与当前事务相关的陈旧数据的实例。

Query对象在Object Relational Tutorial中有详细介绍,并在query_api_toplevel中进一步记录。

添加新的或现有的项目

add() is used to place instances in the session. 对于临时(即全新的)实例,这将在下一次刷新时产生INSERT的效果。对于持久(即由本会话加载)的实例,它们已经存在并且不需要被添加。Instances which are detached (i.e. have been removed from a session) may be re-associated with a session using this method:

user1 = User(name='user1')
user2 = User(name='user2')
session.add(user1)
session.add(user2)

session.commit()     # write changes to the database

要一次向会话添加项目列表,请使用add_all()

session.add_all([item1, item2, item3])

沿着save-update级联的add()操作级联有关更多详细信息,请参阅Cascades部分。

删除¶ T0>

delete()方法将一个实例放入要标记为已删除的会话的对象列表中:

# mark two objects to be deleted
session.delete(obj1)
session.delete(obj2)

# commit (or flush)
session.commit()

从集合中删除

关于delete()的常见混淆是当集合的成员对象被删除时。虽然收集成员被标记为从数据库中删除,但这不会影响集合本身在内存中,直到集合过期。下面我们举例说明,甚至在Address对象被标记为删除之后,即使在刷新之后,它仍然存在于与父User

>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

当上述会话提交时,所有属性都已过期。下一次访问user.addresses将重新加载集合,显示所需的状态:

>>> session.commit()
>>> address in user.addresses
False

通常删除集合中的项目的做法是直接放弃使用delete(),而是使用级联行为自从从父集合中移除对象时自动调用删除。delete-orphan级联完成了这一点,如下例所示:

mapper(User, users_table, properties={
    'addresses':relationship(Address, cascade="all, delete, delete-orphan")
})
del user.addresses[1]
session.flush()

Where above, upon removing the Address object from the User.addresses collection, the delete-orphan cascade has the effect of marking the Address object for deletion in the same way as passing it to delete().

有关级联的详细信息,另请参见Cascades

基于过滤条件删除

Session.delete()的警告是,你需要有一个对象,方便已经删除。查询包括一个delete()方法,该方法根据过滤标准删除:

session.query(User).filter(User.id==7).delete()

Query.delete()方法包含功能,用于将已经在会话中的符合条件的对象“过期”。但是它的确有一些注意事项,包括“delete”和“delete-orphan”级联不能完全表示已经加载的集合。有关更多详细信息,请参阅delete()的API文档。

潮红¶ T0>

Session与其默认配置一起使用时,冲洗步骤几乎总是透明的。具体来说,在提交事务之前,在发出任何单个Query之前以及在commit()调用之前执行刷新。在使用begin_nested()时,也会在发出SAVEPOINT之前发生。

无论自动刷新设置如何,都可以通过发出flush()来强制刷新:

session.flush()

行为的“flush-on-Query”方面可以通过使用标志autoflush=False构造sessionmaker来禁用:

Session = sessionmaker(autoflush=False)

此外,可以随时通过设置autoflush标志暂时禁用自动刷新:

mysession = Session()
mysession.autoflush = False

DisableAutoFlush上有一些自动刷新禁用的配方。

The flush process always occurs within a transaction, even if the Session has been configured with autocommit=True, a setting that disables the session’s persistent transactional state. If no transaction is present, flush() creates its own transaction and commits it. 刷新期间的任何失败都会导致所有事务的回滚。如果会话不处于autocommit=True模式,即使基本事务已经被回滚,但在刷新失败后仍需要对rollback() - 这就是说,所谓的“子事务”的总体嵌套模式始终保持不变。

¶ T0>

commit() is used to commit the current transaction. It always issues flush() beforehand to flush any remaining state to the database; this is independent of the “autoflush” setting. 如果没有交易存在,则会产生错误。请注意,Session的默认行为是“交易”始终存在;这个行为可以通过设置autocommit=True来禁用。在自动提交模式下,可以通过调用begin()方法启动一个事务。

注意

这里的术语“事务”是指Session本身内的一个事务性构造,它可以保持零个或多个实际数据库(DBAPI)事务。一个单独的DBAPI连接开始参与“事务”,因为它首先用于执行SQL语句,然后一直保持到会话级“事务”完成。请参阅Managing Transactions了解更多详情。

commit()的另一个行为是,默认情况下,在提交完成后,所有实例的状态都会到期。这样当下次访问实例时,无论是通过属性访问,还是通过它们存在于Query结果集中,它们都会收到最近的状态。要禁用此行为,请使用expire_on_commit=False配置sessionmaker

通常情况下,加载到Session的实例不会被随后的查询改变;假设当前交易是孤立的,所以只要交易继续,最近加载的状态是正确的。设置autocommit=True在某种程度上对这个模型起作用,因为Session的行为与属性状态完全相同,除非没有事务存在。

回滚

rollback() rolls back the current transaction. 使用默认配置的会话时,会话的回滚后状态如下所示:

  • 所有事务回滚,所有连接返回到连接池,除非会话直接绑定到一个连接,在这种情况下连接仍然保持(但仍然回滚)。
  • 在事务生命周期中,当它们被添加到Session时,最初处于挂起状态的对象被删除,对应于它们的INSERT语句被回滚。他们的属性状态保持不变。
  • 其被标记为删除 T0>交易的寿命内的对象被提升回持久 T1>状态下,对应于它们的DELETE语句被回滚。Note that if those objects were first pending within the transaction, that operation takes precedence instead.
  • 所有未被清除的对象已经过期。

在了解了这个状态后,Session可以在回滚发生后安全地继续使用。

flush()失败时,通常会由于主键,外键或不可空约束等原因而自动发出(rollback()部分失败后可能会继续冲洗)。但是,flush过程总是使用自己的事务分界符(称为subsnsaction),这在Session的文档字符串中有更完整的描述。这意味着即使数据库事务已经回滚,最终用户仍然必须发出rollback()来完全重置Session的状态。

结束¶ T0>

close()方法发出一个expunge_all()releases任何事务/连接资源。当连接返回到连接池时,事务状态也会回滚。