会话基础¶

会议的作用是什么?

一般来说, Session 建立与数据库的所有对话,并为您在其生命周期中加载或关联的所有对象表示一个“保留区域”。它提供入口点来获取 Query 对象,它使用 Session 对象的当前数据库连接,将结果行填充到对象中,然后将这些对象存储在 Session ,在名为 Identity Map -维护每个对象唯一副本的数据结构,其中“唯一”表示“只有一个具有特定主键的对象”。

这个 Session 以基本上无状态的形式开始。一旦发出查询或将其他对象与它一起持久化,它将从 EngineSession 自身或与映射的 Table 正在操作的对象。此连接表示正在进行的事务,在 Session 指示提交或回滚其挂起状态。

对由 Session 被跟踪-在再次查询数据库或提交当前事务之前, 刷新 对数据库的所有挂起更改。这被称为 Unit of Work 模式。

当使用 Session ,需要注意的是,与之关联的对象是 代理对象 到由 Session -有许多事件会导致对象重新访问数据库以保持同步。可以将对象从 Session 并继续使用它们,尽管这种做法有其注意事项。通常情况下,您会将分离的对象与另一个对象关联起来 Session 当您想再次使用它们时,这样它们就可以恢复它们表示数据库状态的正常任务。

正在进行会话

Session 是可以直接实例化的常规python类。但是,为了标准化会话的配置和获取方式,需要 sessionmaker 类通常用于创建顶级 Session 然后可以在整个应用程序中使用的配置,而无需重复配置参数。

用法 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 用于连接资源。

典型设置将关联 sessionmaker 用一个 Engine ,使每个 Session 生成的将使用此 Engine 获取连接资源。可以使用 bind 争论。

编写应用程序时,请将 sessionmaker 全球范围内的工厂。然后,应用程序的其余部分可以使用此工厂作为新的源 Session 实例,保留如何 Session 对象是在一个地方构造的。

这个 sessionmaker 工厂还可以与其他助手一起使用,这些助手通过用户定义的 sessionmaker 然后由助手维护。本节将讨论其中一些帮助者。 我什么时候做一个 Session ,什么时候提交,什么时候关闭? .

向现有sessionmaker()添加其他配置

一个常见的场景是 sessionmaker 在模块导入时调用,但是生成一个或多个 Engine 要与关联的实例 sessionmaker 尚未进行。对于这个用例, 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()

使用备用参数创建临时会话对象

对于应用程序需要创建新的 Session 具有与整个应用程序中通常使用的参数不同的特殊参数,例如 Session 绑定到备用连接源,或 Session 应该有其他的论点,比如 expire_on_commit 与大多数应用程序所需的不同,可以将特定参数传递给 sessionmaker 工厂的 sessionmaker.__call__() 方法。这些参数将覆盖已经放置的任何配置,例如下面的新配置 Session 根据特定的 Connection ::

# 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 是维护外部事务的测试夹具-请参见 将会话加入外部事务(如测试套件) 举个例子。

会话常见问题

到目前为止,许多用户已经对会话产生了疑问。本节介绍了一个小型常见问题解答(请注意,我们还有一个 real FAQ )在使用 Session .

我什么时候做 sessionmaker 是吗?

只是一次,在应用程序的全局范围内的某个地方。它应该被视为应用程序配置的一部分。如果应用程序在一个包中有三个.py文件,则可以将 sessionmaker 线在你 __init__.py 文件;从这一点开始,您的其他模块会说“从mypackage导入会话”。那样的话,其他人都会用 Session() ,该会话的配置由该中心点控制。

如果应用程序启动、导入但不知道它将连接到哪个数据库,则可以绑定 Session 在“Class”(等级)级别,稍后使用 sessionmaker.configure() .

在本节的示例中,我们将经常显示 sessionmaker 在我们实际调用的行的正上方创建 Session . 但这只是举个例子!事实上, sessionmaker 在模块级的某个地方。要实例化的调用 Session 然后将放置在应用程序中数据库对话开始的位置。

我什么时候做一个 Session ,什么时候提交,什么时候关闭?

tl;dr;

  1. 作为一般规则,保持会话的生命周期 独立和外部 访问和/或操作数据库数据的函数和对象。这将大大有助于实现可预测和一致的事务范围。

  2. 确保您清楚地了解事务的开始和结束位置,并保留事务 短的 也就是说,它们以一系列操作结束,而不是无限期地保持打开状态。

A Session 通常在逻辑操作开始时构造,在逻辑操作开始时可能需要数据库访问。

这个 Session 每当它被用来与数据库对话时,只要它开始通信,就开始一个数据库事务。假设 autocommit 标志保留为其建议的默认值 False ,此事务将继续进行,直到 Session 回滚、提交或关闭。这个 Session 如果再次使用新事务,将在上一个事务结束后开始新事务;在此之后,将执行 Session 能够跨多个事务拥有寿命,但一次只能有一个。我们将这两个概念称为 交易范围会话范围 .

这里的含义是,sqlAlchemy ORM鼓励开发人员在其应用程序中建立这两个作用域,不仅包括作用域的开始和结束时间,还包括这些作用域的范围,例如,应该 Session 实例是一个函数或方法中执行流的本地实例,如果它是整个应用程序使用的全局对象,或者介于这两者之间。

开发人员决定这个范围的负担是一个领域,在这个领域中,SQLAlchemy ORM必须对数据库的使用方式有强烈的意见。这个 unit of work 模式是一种随时间累积的变化,并定期刷新它们,使内存状态与本地事务中已知的状态保持同步。只有当有意义的事务范围就位时,此模式才有效。

通常很难确定开始和结束 Session 尽管各种各样的应用程序架构可能会带来挑战性的情况。

一个常见的选择是拆除 Session 同时事务结束,这意味着事务和会话范围相同。这是一个很好的选择,因为它消除了将会话作用域视为独立于事务作用域的需要。

虽然对于如何确定事务范围没有一个适合所有人的建议,但是有一些常见的模式。尤其是如果你正在编写一个Web应用程序,那么这个选择就已经确定了。

Web应用程序是最简单的情况,因为这样的应用程序已经围绕一个单一的、一致的范围构建——这是 请求 ,表示来自浏览器的传入请求,处理该请求以形成响应,最后将该响应传递回客户端。将Web应用程序与 Session 那么直接的任务就是将 Session 与请求的一致。这个 Session 可以在请求开始时建立,或者使用 lazy initialization 在需要时立即建立一个模式。然后请求继续进行,一些系统就位,应用程序逻辑可以访问当前的 Session 以与实际请求对象的访问方式相关联的方式。当请求结束时, Session 通常通过使用Web框架提供的事件挂钩,也会被拆掉。由使用的事务 Session 也可以在此时提交,或者应用程序可以选择显式提交模式,只在有保证的情况下提交那些请求,但仍然会删除 Session 结束时无条件。

一些Web框架包括用于帮助调整 Session 一个Web请求。这包括以下产品: Flask-SQLAlchemy ,与flask web框架结合使用,以及 Zope-SQLAlchemy ,通常与金字塔框架一起使用。SQLAlchemy建议在可用时使用这些产品。

在没有提供集成库或集成库不足的情况下,SQLAlchemy包含其自己的“helper”类,称为 scoped_session . 有关此对象用法的教程位于 上下文/线程本地会话 . 它提供了将 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()

最全面的方法,建议用于更实际的应用程序,将尽量保持会话、事务和异常管理的细节与正在执行其工作的程序的细节不同。例如,我们可以使用 context manager ::

### 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)

会话是缓存吗?

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

此外,默认情况下,会话使用弱引用存储对象实例。这也破坏了将会话用作缓存的目的。

这个 Session 不是设计为全局对象,每个人都从中作为对象的“注册表”进行查询。这更像是 二级缓存 . SQLAlchemy提供了一种模式,用于实现二级缓存,使用 dogpile.cache ,通过 Dogpile缓存 例子。

我怎么才能拿到 Session 为了某个目标?

使用 object_session() ClassMethod可用于 Session ::

session = Session.object_session(someobject)

更新者 运行时检查API 系统也可以使用:

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

会话线程安全吗?

这个 Session 非常适合用于 non-concurrent 时尚,通常一次只穿一条线。

这个 Session 应该以这样的方式使用:在单个事务中,对于单个操作系列,存在一个实例。获得这种效果的一种权宜之计是将 Session 使用当前线程(请参见 上下文/线程本地会话 作为背景)。另一种方法是使用 Session 在函数之间传递,否则不与其他线程共享。

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

确保 Session 一次只在一个并发线程中使用被称为“无共享”并发方法。但实际上,不是分享 Session 意味着一个更重要的模式;它不仅仅意味着 Session 对象本身,但也 与该会话关联的所有对象 必须保持在单个并发线程的范围内。与 Session 基本上是通过数据库连接访问的数据库行中的数据代理,因此就像 Session 整个对象集本身实际上只是数据库连接(或连接)的大型代理。最终,我们远离并发访问的主要是DBAPI连接本身;但是由于 Session 与之相关联的所有对象都是DBAPI连接的代理,整个图对于并发访问基本上是不安全的。

如果事实上有多个线程参与同一个任务,那么您可以考虑在这些线程之间共享会话及其对象;但是,在这种极不寻常的情况下,应用程序需要确保实现适当的锁定方案,以便 同时发生的 访问 Session 或者它的状态。在这种情况下,更常见的方法是保持 Session 每个并发线程,但改为 copy 对象来自一个 Session 对另一个,通常使用 Session.merge() 方法将一个对象的状态复制到另一个对象的本地新对象中 Session .

使用会话的基础知识

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

查询

这个 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 对象在 对象关系教程 ,并进一步记录在 查询API .

添加新项目或现有项目

add() 用于在会话中放置实例。为了 瞬态 (即全新的)实例,这将具有在下次刷新时为这些实例进行插入的效果。例如 持久的 (即由本次会话加载),它们已经存在,不需要添加。实例是 独立的 (即已从会话中删除)可以使用以下方法与会话重新关联:

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])

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

删除

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

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

# commit (or flush)
session.commit()

从集合和标量关系中删除引用的对象

通常,ORM在刷新过程中从不修改集合或标量关系的内容。这意味着,如果你的班级 relationship() 它指的是对象集合,或对单个对象(如多对一)的引用,当刷新过程发生时,不会修改此属性的内容。相反,如果 Session 之后过期,或者通过的提交过期行为 Session.commit() 或通过明确使用 Session.expire() ,与之关联的给定对象上的引用对象或集合 Session 将被清除,并在下次访问时重新加载。

此行为不应与刷新进程对引用外键和主键列的列绑定属性的影响混淆;这些属性在刷新过程中被大量修改,因为这些是刷新进程打算管理的属性。也不应将其与backreferences的行为混淆,如中所述 链接与backref的关系 ;backreference事件将修改集合或标量属性引用,但此行为发生在直接操作相关集合和对象引用期间,这些集合和对象引用在调用应用程序中是显式的,不在刷新过程中。

在这种行为中出现的一种常见的混淆包括使用 delete() 方法。什么时候? Session.delete() 对对象和 Session 刷新后,该行将从数据库中删除。通过外键引用目标行的行,假定使用 relationship() 在两个映射对象类型之间,还将看到它们的外键属性更新为空,或者如果设置了删除层叠,则相关行也将被删除。但是,即使与已删除对象相关的行本身也可能被修改, no changes occur to relationship-bound collections or object references on the objects 参与刷新本身范围内的操作。这意味着,如果对象是相关集合的成员,则在该集合过期之前,它仍然存在于Python端。同样,如果对象是通过多对一或一对一从另一个对象引用的,那么该引用将仍然存在于该对象上,直到该对象过期。

下面,我们举例说明 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

有一个拦截的方法 Session.delete() 并自动调用此过期;请参见 ExpireRelationshipOnFKChange 为此。但是,在集合中删除项的通常做法是放弃使用 delete() 直接调用,而使用级联行为自动调用从父集合中移除对象后的删除操作。这个 delete-orphan Cascade实现了这一点,如下面的示例所示:

class User(Base):
    __tablename__ = 'user'

    # ...

    addresses = relationship(
        "Address", cascade="all, delete, delete-orphan")

# ...

del user.addresses[1]
session.flush()

在上面的位置,移除 Address 对象从 User.addresses 收藏 delete-orphan 级联具有标记 Address 对象的删除方式与将其传递给 delete() .

这个 delete-orphan 层叠还可以应用于多对一或一对一关系,这样当对象从其父级取消关联时,它也会自动标记为删除。使用 delete-orphan 在多对一或一对一上层叠需要附加标志 relationship.single_parent 它调用一个断言,即此相关对象不会同时与任何其他父对象共享::

class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete, delete-orphan",
        single_parent=True)

如果假设 Preference 对象从中移除 User ,它将在刷新时被删除::

some_user.preference = None
session.flush()  # will delete the Preference object

参见

级联 有关级联的详细信息。

根据筛选条件删除

告诫 Session.delete() 你需要有一个方便的对象才能删除。查询包括 delete() 根据筛选条件删除的方法:

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

这个 Query.delete() 方法包括使会话中已有的符合条件的对象“过期”的功能。但是,它确实有一些警告,包括对于已经加载的集合,“删除”和“删除孤立”级联不能完全表示。参见API文档 delete() 了解更多详细信息。

刷新

Session 与默认配置一起使用时,刷新步骤几乎总是透明完成的。具体来说,刷新发生在任何个体之前。 Query 发布,以及在 commit() 在提交事务之前调用。当 begin_nested() 使用。

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

session.flush()

可以通过构造 sessionmaker 带旗 autoflush=False ::

Session = sessionmaker(autoflush=False)

此外,通过设置 autoflush 随时标记:

mysession = Session()
mysession.autoflush = False

更方便的是,可以使用 Session.no_autoflush ::

with mysession.no_autoflush:
    mysession.add(some_object)
    mysession.flush()

刷新过程 总是 在事务中发生,即使 Session 已配置为 autocommit=True ,禁用会话的持久事务状态的设置。如果没有交易, flush() 创建自己的事务并提交它。刷新期间的任何失败都将始终导致回滚任何存在的事务。如果会话不在 autocommit=True 模式,对 rollback() 在刷新失败后是必需的,即使基础事务已经回滚了——这是为了使所谓的“子事务”的整体嵌套模式始终保持不变。

提交

commit() 用于提交当前事务。总是有问题 flush() 预先将任何剩余状态刷新到数据库;这与“自动刷新”设置无关。如果不存在事务,则会引发错误。注意的默认行为 Session 是否始终存在“事务”;可以通过设置禁用此行为 autocommit=True . 在自动提交模式下,可以通过调用 begin() 方法。

注解

这里的术语“事务”是指 Session 它本身可以维护零个或更多的实际数据库(DBAPI)事务。单个DBAPI连接开始参与“事务”,因为它首先用于执行SQL语句,然后保持存在,直到会话级“事务”完成。见 管理交易 更多细节。

另一种行为 commit() 默认情况下,它将在提交完成后过期所有存在实例的状态。这样,在下次访问实例时,可以通过属性访问,也可以通过实例在 Query 结果集,它们接收最新状态。要禁用此行为,请配置 sessionmaker 具有 expire_on_commit=False .

通常,实例加载到 Session 不会被后续查询更改;假设当前事务是隔离的,因此只要事务继续,最近加载的状态是正确的。设置 autocommit=True 在某种程度上与此模型相悖,因为 Session 在属性状态方面的行为完全相同,除非不存在事务。

回滚

rollback() 回滚当前事务。对于默认配置的会话,会话的回滚后状态如下:

  • 所有事务都会回滚,所有连接都会返回到连接池,除非会话直接绑定到某个连接,在这种情况下,连接仍然保持不变(但仍会回滚)。

  • 最初在 悬而未决的 说明何时将它们添加到 Session 在事务的生命周期内将被删除,对应于它们的insert语句将被回滚。其属性的状态保持不变。

  • 标记为的对象 删除 在事务的生命周期内,将提升回 持久的 状态,与要回滚的DELETE语句相对应。请注意,如果这些对象是第一个 悬而未决的 在事务中,该操作优先。

  • 所有未删除的对象都已完全过期。

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

当A flush() 失败,通常是由于主键、外键或“不可为空”的约束冲突等原因, rollback() 自动发出(部分失败后当前无法继续刷新)。但是,刷新进程总是使用自己的事务分界符,称为 子事务 ,在文档字符串中更详细地描述了 Session . 这里的意思是,即使数据库事务已回滚,最终用户仍必须发出 rollback() 完全重置 Session .

关闭

这个 close() 方法问题A expunge_all()releases 任何事务/连接资源。当连接返回到连接池时,事务状态也将回滚。