SqlAlchemy 2.0 中文文档(二十二)(3)

简介: SqlAlchemy 2.0 中文文档(二十二)

SqlAlchemy 2.0 中文文档(二十二)(2)https://developer.aliyun.com/article/1560448


查询

查询的主要方式是利用 select() 构造创建一个 Select 对象,然后使用诸如 Session.execute()Session.scalars() 等方法执行以返回结果。结果然后以 Result 对象的形式返回,包括诸如 ScalarResult 等子变体。

SQLAlchemy ORM 查询指南提供了完整的 SQLAlchemy ORM 查询指南。以下是一些简要示例:

from sqlalchemy import select
from sqlalchemy.orm import Session
with Session(engine) as session:
    # query for ``User`` objects
    statement = select(User).filter_by(name="ed")
    # list of ``User`` objects
    user_obj = session.scalars(statement).all()
    # query for individual columns
    statement = select(User.name, User.fullname)
    # list of Row objects
    rows = session.execute(statement).all()

从版本 2.0 开始更改:“2.0”样式查询现在是标准的。请参阅 2.0 迁移 - ORM 使用 查看从 1.x 系列迁移的注意事项。

另请参阅

ORM 查询指南

添加新的或现有的项目

Session.add() 用于将实例放入会话中。对于临时(即全新)的实例,这将在下一次刷新时导致对这些实例进行 INSERT。对于持久(即由此会话加载的)实例,它们已经存在,不需要添加。分离(即已从会话中移除的)实例可以使用此方法重新关联到会话中:

user1 = User(name="user1")
user2 = User(name="user2")
session.add(user1)
session.add(user2)
session.commit()  # write changes to the database

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

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

Session.add() 操作级联执行save-update级联。更多详情请参阅 Cascades 部分。

删除

Session.delete() 方法将一个实例放入会话的待删除对象列表中:

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

Session.delete() 标记一个对象为删除状态,这将导致为每个受影响的主键发出 DELETE 语句。在挂起的删除被刷新之前,“delete”标记的对象存在于Session.deleted集合中。删除后,它们将从Session中移除,在事务提交后,这将变得永久。

Session.delete()操作相关的各种重要行为,尤其是与其他对象和集合的关系如何处理的行为。有关此操作的更多信息,请参见 Cascades 部分,但通常规则如下:

  • 通过relationship()指令与已删除对象相关联的映射对象对应的行默认不会被删除。如果这些对象对被删除的行有一个外键约束,则这些列将设置为 NULL。如果这些列是非空的,这将导致约束违反。
  • 要将“SET NULL”更改为相关对象行的 DELETE,请使用relationship() 上的删除级联。
  • 作为“多对多”表链接的表中的行,通过relationship.secondary参数,所有情况下都会被删除,当它们引用的对象被删除时。
  • 当相关对象包含返回到正在删除的对象的外键约束,并且它们所属的相关集合当前未加载到内存中时,工作单元将发出  SELECT 来获取所有相关行,以便它们的主键值可以用于发出 UPDATE 或 DELETE 语句在这些相关行上。通过这种方式,ORM  将在没有进一步指示的情况下执行 ON DELETE CASCADE 的功能,即使这在 Core ForeignKeyConstraint对象上进行了配置。
  • relationship.passive_deletes参数可用于调整此行为,并更自然地依赖于“ON DELETE CASCADE”;当设置为 True 时,此 SELECT 操作将不再发生,但是仍将对本地存在的行进行显式的 SET NULL 或 DELETE。将relationship.passive_deletes设置为字符串"all"将禁用所有相关对象的更新/删除。
  • 当标记为删除的对象发生删除时,并不会自动从引用它的集合或对象引用中删除该对象。当Session过期时,这些集合可能会再次加载,以便对象不再存在。但是,最好不要对这些对象使用Session.delete(),而是应该从其集合中移除对象,然后使用 delete-orphan,以使其作为该集合移除的副作用而被删除。请参见部分关于删除 - 从集合和标量关系中删除引用的对象以获取示例。

参见

delete - 描述了“级联删除”,当主对象被删除时,会标记相关对象为删除。

delete-orphan - 描述了“孤立删除级联”,当它们从其主对象中取消关联时,会标记相关对象为删除。

删除说明 - 删除从集合和标量关系引用的对象 - 有关Session.delete()的重要背景,涉及在内存中刷新关系。

刷新

Session以其默认配置使用时,刷新步骤几乎总是在透明地完成。具体来说,在由Query或 2.0 风格的Session.execute()调用引发任何单个 SQL 语句的情况下,以及在提交事务之前的Session.commit()调用之前,都会发生刷新。它还在使用Session.begin_nested()时发出 SAVEPOINT 前发生。

可以通过调用Session.flush()方法随时强制执行Session刷新:

session.flush()

在某些方法的范围内自动发生的刷新称为自动刷新。自动刷新被定义为在以下方法开始时发生的可配置的自动刷新调用:

  • 当针对启用 ORM 的 SQL 构造执行方法(如针对 ORM 实体和/或 ORM 映射属性的select()对象)使用Session.execute()和其他执行 SQL 的方法时
  • 当调用Query来将 SQL 发送到数据库时
  • 在查询数据库之前的Session.merge()方法内
  • 当对象被刷新
  • 当针对未加载对象属性进行 ORM 惰性加载 操作时。

也有一些刷新会无条件发生;这些点位于关键事务边界内,包括:

  • Session.commit()方法的过程中
  • 当调用Session.begin_nested()
  • 当使用Session.prepare() 2PC 方法时。

作为前面项目的一部分应用的自动刷新行为可以通过构造一个传递SessionsessionmakerSession.autoflush参数为False来禁用:

Session = sessionmaker(autoflush=False)

另外,自动刷新可以在使用Session的流程中暂时禁用,使用Session.no_autoflush上下文管理器:

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

重申一下: 当调用事务方法(例如Session.commit()Session.begin_nested())时,刷新过程总是发生,而不管任何“自动刷新”设置如何,当Session还有未处理的待处理更改时。

由于Session仅在 DBAPI 事务的上下文中调用数据库的  SQL,所有“刷新”操作本身仅发生在数据库事务内(取决于数据库事务的隔离级别),前提是 DBAPI  不处于驱动程序级别的自动提交模式。这意味着假设数据库连接在其事务设置中提供了原子性,如果刷新中的任何单个 DML 语句失败,整个操作都将被回滚。

在刷新过程中发生故障时,为了继续使用相同的Session,即使底层事务已经回滚(即使数据库驱动程序在技术上处于驱动程序级别的自动提交模式),也需要在刷新失败后显式调用Session.rollback(),这样可以保持所谓的“子事务”的整体嵌套模式的一致性。“由于刷新期间发生的先前异常,此会话的事务已被回滚。”(或类似)FAQ 部分包含了对此行为的更详细描述。

另请参阅

“由于在刷新期间发生的先前异常,此会话的事务已回滚。”(或类似) - 关于在刷新失败时为什么必须调用 Session.rollback() 的更多背景信息。

按主键获取

由于 Session 使用的是一个 标识映射,它通过主键引用当前内存中的对象,因此 Session.get() 方法被提供作为一种通过主键定位对象的方法,首先查找当前标识映射,然后查询数据库以获取不存在的值。例如,要定位具有主键标识 (5, )User 实体:

my_user = session.get(User, 5)

Session.get() 还包括用于复合主键值的调用形式,可以作为元组或字典传递,以及允许特定加载器和执行选项的附加参数。参见 Session.get() 以获取完整的参数列表。

另请参阅

Session.get()

过期 / 刷新

在使用 Session  时经常会遇到的一个重要考虑因素是处理从数据库加载的对象上存在的状态,以保持它们与事务的当前状态同步。SQLAlchemy ORM 是基于一个  标识映射 的概念,即当从 SQL 查询中“加载”对象时,将维护一个与特定数据库标识相对应的唯一 Python  对象实例。这意味着如果我们发出两个单独的查询,每个查询都针对同一行,并获得一个映射对象,则这两个查询将返回相同的 Python 对象:

>>> u1 = session.scalars(select(User).where(User.id == 5)).one()
>>> u2 = session.scalars(select(User).where(User.id == 5)).one()
>>> u1 is u2
True

由此可见,当 ORM 从查询中获取行时,它将跳过已加载对象的属性的填充。这里的设计假设是假设一个完全隔离的事务,然后根据事务的隔离程度,应用程序可以根据需要采取步骤从数据库事务中刷新对象。我正在使用我的会话重新加载数据,但它看不到我在其他地方提交的更改的 FAQ 条目更详细地讨论了这个概念。

当将 ORM 映射对象加载到内存中时,有三种常见方法可以使用当前事务的新数据刷新其内容:

  • expire() 方法 - Session.expire() 方法将擦除对象的选定或所有属性的内容,以便在下次访问时从数据库加载它们,例如使用延迟加载模式:
session.expire(u1)
u1.some_attribute  # <-- lazy loads from the transaction
  • refresh() 方法 - 与之密切相关的是Session.refresh()方法,它执行Session.expire()方法的所有操作,但还会立即发出一个或多个 SQL 查询以实际刷新对象的内容:
session.refresh(u1)  # <-- emits a SQL query
u1.some_attribute  # <-- is refreshed from the transaction
  • populate_existing() 方法或执行选项 - 这现在是一个在 Populate Existing 文档中记录的执行选项;在传统形式中,它在Query对象上作为Query.populate_existing()方法找到。无论以哪种形式,此操作都表示从查询返回的对象应无条件地从数据库中重新填充:
u2 = session.scalars(
    select(User).where(User.id == 5).execution_options(populate_existing=True)
).one()

有关刷新 / 过期概念的进一步讨论,请参阅刷新 / 过期。

另请参阅

刷新 / 过期

我正在使用我的 Session 重新加载数据,但它没有看到我在其他地方提交的更改

使用任意 WHERE 子句的 UPDATE 和 DELETE

SQLAlchemy 2.0 包括增强功能,用于发出几种 ORM 启用的 INSERT、UPDATE 和 DELETE 语句。请参阅 ORM-Enabled INSERT, UPDATE, and DELETE statements 文档。

另请参阅

ORM-Enabled INSERT, UPDATE, and DELETE statements

使用自定义 WHERE 条件的 ORM UPDATE 和 DELETE

自动开始

Session对象具有称为autobegin的行为。这表示当使用Session执行任何工作时,无论涉及修改Session的内部状态以进行对象状态更改,还是涉及需要数据库连接的操作,Session将在内部认为自己处于“事务”状态。

Session 第一次被构造时,没有事务状态存在。当调用诸如 Session.add()Session.execute() 这样的方法时,事务状态会自动开始,或者类似地,如果执行 Query 来返回结果(最终使用 Session.execute()),或者如果在 持久化 对象上修改属性。

可以通过访问 Session.in_transaction() 方法来检查事务状态,该方法返回 TrueFalse,指示“自动开始”步骤是否已执行。虽然通常不需要,但 Session.get_transaction() 方法将返回表示此事务状态的实际 SessionTransaction 对象。

Session 的事务状态也可以通过显式调用 Session.begin() 方法来启动。当调用此方法时,Session 无条件地处于“事务”状态。Session.begin() 可以像 框架化一个 begin / commit / rollback 块 中描述的那样用作上下文管理器。

禁用 Autobegin 以防止隐式事务

“自动开始”行为可以通过将Session.autobegin参数设置为False来禁用。通过使用此参数,Session将要求显式调用Session.begin()方法。在构造时,以及在调用任何Session.rollback()Session.commit()Session.close()方法之后,Session不会隐式开始任何新事务,并且如果尝试在未首先调用Session.begin()的情况下使用Session,将会引发错误:

with Session(engine, autobegin=False) as session:
    session.begin()  # <-- required, else InvalidRequestError raised on next call
    session.add(User(name="u1"))
    session.commit()
    session.begin()  # <-- required, else InvalidRequestError raised on next call
    u1 = session.scalar(select(User).filter_by(name="u1"))

新版本 2.0 中:添加了 Session.autobegin,允许禁用“自动开始”行为 #### 禁用自动开始以防止隐式事务

“自动开始”行为可以通过将Session.autobegin参数设置为False来禁用。通过使用此参数,Session将要求显式调用Session.begin()方法。在构造时,以及在调用任何Session.rollback()Session.commit()Session.close()方法之后,Session不会隐式开始任何新事务,并且如果尝试在未首先调用Session.begin()的情况下使用Session,将会引发错误:

with Session(engine, autobegin=False) as session:
    session.begin()  # <-- required, else InvalidRequestError raised on next call
    session.add(User(name="u1"))
    session.commit()
    session.begin()  # <-- required, else InvalidRequestError raised on next call
    u1 = session.scalar(select(User).filter_by(name="u1"))

新版本 2.0 中:添加了 Session.autobegin,允许禁用“自动开始”行为

提交

Session.commit() 用于提交当前事务。在其核心,这表示对所有当前具有正在进行事务的数据库连接发出COMMIT;从 DBAPI 的角度来看,这意味着在每个 DBAPI 连接上调用 connection.commit() DBAPI 方法。

Session没有处于事务中时,表示自上次调用Session.commit()以来,对此Session没有调用操作,该方法将开始并提交一个仅“逻辑”事务,通常不会影响数据库,除非检测到待定的刷新更改,但仍然会调用事件处理程序和对象过期规则。

Session.commit() 操作在发出相关数据库连接的 COMMIT 之前无条件地发出 Session.flush()。如果未检测到待定更改,则不会向数据库发出 SQL。此行为不可配置,并且不受 Session.autoflush 参数的影响。

在此之后,假设Session绑定到一个Engine,则Session.commit()将 COMMIT 实际的数据库事务,如果已启动。提交后,与该事务关联的Connection对象将关闭,导致其底层的 DBAPI 连接被释放回与Session绑定的Engine相关联的连接池。

对于绑定到多个引擎(例如在分区策略中描述的那样)的Session,相同的 COMMIT 步骤将为每个在“逻辑”事务中使用的Engine / Connection 进行。除非启用了两阶段功能,否则这些数据库事务之间是不协调的。

还有其他的连接交互模式,可以直接将Session绑定到Connection上;在这种情况下,假定存在外部管理的事务,并且在这种情况下不会自动发出真正的 COMMIT;有关此模式的背景信息,请参见加入外部事务的会话(例如测试套件)一节。

最后,Session内的所有对象在事务关闭时都会被过期。这样,在下次访问实例时,无论是通过属性访问还是通过它们存在于 SELECT 结果中,它们都会接收到最新的状态。此行为可以通过Session.expire_on_commit标志来控制,当不希望这种行为时,可以将其设置为False

另请参阅

自动开始

回滚

Session.rollback()回滚当前事务(如果有)。当没有事务时,该方法会静默地通过。

默认配置的会话(session)后,会话的事务回滚状态,其后续是通过自动开始或显式调用Session.begin()方法开始事务后的情况如下:

  • 数据库事务将被回滚。对于绑定到单个EngineSession,这意味着针对当前正在使用的最多一个Connection发出 ROLLBACK。对于绑定到多个Engine对象的Session对象,针对所有被检出的Connection对象发出 ROLLBACK。

  • 数据库连接将被释放。这遵循了提交中指出的与连接相关的相同行为,其中从Engine对象获取的Connection对象将被关闭,导致 DBAPI 连接被释放到Engine中的连接池中。如果有新的事务开始,则会从Engine中检出新连接。

  • 对于直接绑定到ConnectionSession,如将会话加入外部事务(比如测试套件),此Connection上的回滚行为将遵循Session.join_transaction_mode参数指定的行为,这可能涉及回滚保存点或发出真正的 ROLLBACK。

  • 在事务生命周期内将挂起状态的对象从添加到Session中时的状态是被移除的,对应于其 INSERT 语句的回滚。它们的属性状态保持不变。

  • 已删除对象在事务生命周期内被重新提升到持久化状态,对应其 DELETE 语句被回滚。请注意,如果这些对象首先在事务内为挂起状态,那么该操作将优先进行。

  • 所有未清除的对象都将完全过期 - 这与Session.expire_on_commit设置无关。

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

从 1.4 版本开始更改:Session对象现在具有延迟“开始”行为,如 autobegin 中所述。如果未开始任何事务,则Session.commit()Session.rollback()等方法不会产生任何效果。在 1.4 版本之前不会观察到此行为,因为在非自动提交模式下,事务始终会隐式存在。

Session.flush()失败时,通常是由于主键、外键或“非空”约束违反等原因,将自动发出 ROLLBACK(目前不可能在部分失败后继续 flush)。但是,此时Session处于一种称为“不活跃”的状态,并且调用应用程序必须始终显式调用Session.rollback()方法,以使Session可以恢复到可用状态(也可以简单地关闭和丢弃)。有关详细讨论,请参阅“由于刷新期间发生先前异常,此会话的事务已被回滚。”(或类似)的常见问题解答条目。

另请参阅

自动开始

结束

Session.close() 方法会调用 Session.expunge_all() 方法,该方法会将会话中的所有 ORM 映射对象移除,并且释放与其绑定的 Engine 对象的所有事务/连接资源。当连接返回到连接池时,事务状态也会回滚。

默认情况下,当 Session 被关闭时,它实际上处于最初构造时的原始状态,并且可以再次使用。从这个意义上讲,Session.close() 方法更像是一个“重置”回到干净状态,而不太像一个“数据库关闭”方法。在这种操作模式下,方法 Session.reset()Session.close() 的别名,并且以相同的方式运行。

Session.close() 的默认行为可以通过设置参数 Session.close_resets_onlyFalse 来更改,表示在调用方法 Session.close() 后,Session 不能被重复使用。在这种操作模式下,当 Session.close_resets_only 设置为 True 时,方法 Session.reset() 将允许多次“重置”会话,表现得像 Session.close()

新功能在版本 2.0.22 中添加。

强烈建议通过调用 Session.close() 在结束时限制 Session 的范围,特别是如果没有使用 Session.commit()Session.rollback() 方法。Session 可以作为上下文管理器使用,以确保调用 Session.close()

with Session(engine) as session:
    result = session.execute(select(User))
# closes session automatically

自 1.4 版更改:Session 对象具有延迟“开始”行为,如 autobegin 中所述,在调用 Session.close() 方法后不会立即开始新的事务。

Session 常见问题

到了这个时候,许多用户已经对会话有了疑问。本节介绍了使用 Session 时常见问题的迷你常见问题解答(请注意我们也有一个真实常见问题解答)。

我什么时候使用 sessionmaker

只需一次,在你应用程序的全局范围内的某个地方。它应该被视为应用程序配置的一部分。如果你的应用程序在一个包中有三个 .py 文件,你可以将 sessionmaker 行放在你的 __init__.py 文件中;从那时起,你的其他模块会说“from mypackage import Session”。这样,其他人只需使用 Session(),而该会话的配置由该中心点控制。

如果你的应用程序启动,进行导入,但不知道将连接到什么数据库,你可以稍后在“类”级别将 Session 绑定到引擎,使用 sessionmaker.configure()

在本节的示例中,我们经常会在实际调用Session的代码行的上方展示sessionmaker的创建。但那只是为了举例而已!在实际情况中,sessionmaker可能会在模块级别的某处。然后,实例化Session的调用将放置在应用程序开始数据库交谈的地方。

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

通常在预期可能需要数据库访问的逻辑操作的开始处构造Session

每当使用Session与数据库通信时,Session都会在开始通信时启动数据库事务。此事务将持续进行,直到Session被回滚、提交或关闭。如果再次使用它,则Session将开始一个新的事务,继续上一个事务结束的位置;因此,Session能够在多个事务中具有生命周期,尽管一次只能有一个事务。我们将这两个概念称为事务范围会话范围

通常情况下,确定Session的范围的最佳时机并不是很困难,尽管可能存在各种各样的应用架构,这可能会引入具有挑战性的情况。

一些示例场景包括:

  • Web 应用程序。在这种情况下,最好利用正在使用的 Web 框架提供的 SQLAlchemy 集成。或者,基本模式是在 Web 请求开始时创建一个Session,在执行 POST、PUT 或 DELETE 的 Web 请求结束时调用Session.commit() 方法,然后在 Web 请求结束时关闭会话。通常也是一个好主意将Session.expire_on_commit设置为 False,这样视图层中来自Session的对象的后续访问就不需要发出新的 SQL 查询来刷新对象,如果事务已经提交。
  • 一个产生子进程的后台守护程序将希望为每个子进程创建一个本地的Session,在该子进程处理的“作业”的生命周期内使用该Session,然后在作业完成时将其关闭。
  • 对于命令行脚本,应用程序将创建一个单一的全局Session,当程序开始工作时建立,完成任务时立即提交。
  • 对于 GUI 接口驱动的应用程序,Session 的范围最好在用户生成的事件范围内,比如按钮点击。或者,范围可能对应于显式用户交互,比如用户“打开”一系列记录,然后“保存”它们。

作为一般规则,应用程序应该在外部管理会话的生命周期,而不是在处理特定数据的函数中。这是一种基本的关注点分离,使得特定于数据的操作与它们访问和操作数据的上下文无关。

例如不要这样做

### this is the **wrong way to do it** ###
class ThingOne:
    def go(self):
        session = Session()
        try:
            session.execute(update(FooBar).values(x=5))
            session.commit()
        except:
            session.rollback()
            raise
class ThingTwo:
    def go(self):
        session = Session()
        try:
            session.execute(update(Widget).values(q=18))
            session.commit()
        except:
            session.rollback()
            raise
def run_my_program():
    ThingOne().go()
    ThingTwo().go()

将会话的生命周期(通常是事务)分开并置于外部。下面的示例说明了这可能是什么样子,并且另外利用了 Python 上下文管理器(即with:关键字)来自动管理Session及其事务的范围:

### this is a **better** (but not the only) way to do it ###
class ThingOne:
    def go(self, session):
        session.execute(update(FooBar).values(x=5))
class ThingTwo:
    def go(self, session):
        session.execute(update(Widget).values(q=18))
def run_my_program():
    with Session() as session:
        with session.begin():
            ThingOne().go(session)
            ThingTwo().go(session)

从版本 1.4 开始:Session 可以作为上下文管理器使用,而无需使用外部帮助函数。

会话是缓存吗?

是的……不。它在某种程度上被用作缓存,因为它实现了标识映射模式,并将对象按其主键键入存储。但它不执行任何查询缓存。这意味着,即使 Foo(name='bar') 就在那里,位于标识映射中,如果你说 session.scalars(select(Foo).filter_by(name='bar')),会话也不知道那个。它必须向数据库发出 SQL,获取行,然后当它看到行中的主键时,然后它才能查看本地标识映射,并看到对象已经在那里。只有当你说 query.get({some primary key}) 时,Session 才不需要发出查询。

此外,默认情况下,会话使用弱引用存储对象实例。这也使得将会话用作缓存失去了意义。

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

如何获取某个对象的 Session

使用 Session.object_session() 类方法,该方法可用于 Session

session = Session.object_session(someobject)

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

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

会话是否线程安全?AsyncSession 在并发任务中是否安全共享?

Session 是一个可变的,有状态的对象,表示一个单个的数据库事务。因此,Session 的实例不能在并发线程或 asyncio 任务之间共享,除非进行了仔细的同步Session 应该以非并发的方式使用,也就是说,一个特定的 Session 实例一次只应该在一个线程或任务中使用。

当使用 SQLAlchemy 的 asyncio 扩展中的AsyncSession对象时,此对象仅是Session的一个薄代理,并且相同的规则适用;它是一个非同步、可变、有状态的对象,因此安全在多个 asyncio 任务中同时使用单个AsyncSession实例。

一个SessionAsyncSession的实例表示一个单一的逻辑数据库事务,每次仅引用一个特定的EngineAsyncEngine的单一Connection(注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然每个引擎只有一个连接在起作用)。

事务中的数据库连接也是一个有状态对象,旨在以非并发、顺序方式进行操作。命令按顺序在连接上发出,数据库服务器以发出的确切顺序处理它们。当Session发出针对此连接的命令并接收结果时,Session本身正在通过与此连接上存在的命令和数据状态相一致的内部状态变化进行过渡;这些状态包括是否开始、提交或回滚事务,如果有的话,正在起作用的 SAVEPOINT,以及将数据库行的状态与本地 ORM 映射对象的细粒度同步。

在设计并发数据库应用程序时,适当的模型是每个并发任务/线程都使用自己的数据库事务。这就是为什么在讨论数据库并发问题时,使用的标准术语是多个并发事务。在传统的关系数据库管理系统中,没有单个数据库事务的类似物,它可以同时接收和处理多个命令。

因此,SQLAlchemy 的 SessionAsyncSession 的并发模型是 每个线程一个 Session,每个任务一个 AsyncSession。一个使用多个线程或多个 asyncio 任务的应用程序,例如在使用 asyncio.gather() 这样的 API 时,会希望确保每个线程都有自己的Session,每个 asyncio 任务都有自己的 AsyncSession

确保此用法的最佳方法是在位于线程或任务内的顶级 Python 函数中本地使用 标准上下文管理器模式,这将确保SessionAsyncSession的生命周期在本地范围内维护。

对于那些受益于拥有“全局”Session的应用程序,在不方便将Session对象传递给需要它的特定函数和方法的情况下,scoped_session方法可以提供一个“线程本地”的Session对象;详见 上下文/线程本地会话 部分。在 asyncio 上下文中,async_scoped_session对象是 scoped_session 的 asyncio 类比,然而更难配置,因为它需要一个自定义的“上下文”函数。

在什么时候我会使用sessionmaker

只需要一次,在应用程序的全局范围内的某处。它应被视为应用程序配置的一部分。如果你的应用程序在一个包中有三个 .py 文件,你可以将sessionmaker行放在你的 __init__.py 文件中;从那时起,你的其他模块会说“from mypackage import Session”。这样,其他人只需使用Session(),而该会话的配置由该中心点控制。

如果你的应用程序启动,导入了模块,但不知道要连接到哪个数据库,你可以在之后将Session在“类”级别绑定到引擎上,使用sessionmaker.configure()

在本节的示例中,我们经常会在实际调用Session的代码行的上方创建sessionmaker。但那只是为了举例说明!实际上,sessionmaker应该在模块级别的某个地方。然后,在应用程序中开始数据库会话的地方会放置对Session的实例化调用。

我什么时候构建Session,什么时候提交它,什么时候关闭它?

Session通常是在可能需要访问数据库的逻辑操作开始时构建的。

每当使用Session与数据库通信时,会立即开始一个数据库事务。该事务会一直持续到Session被回滚、提交或关闭。如果再次使用了Session,则会开始一个新的事务,这意味着Session可以跨越多个事务的生命周期,尽管一次只能有一个事务。我们将这两个概念称为事务范围会话范围

通常很容易确定开始和结束Session范围的最佳时机,尽管可能出现各种各样的应用程序架构,会引入具有挑战性的情况。

一些示例场景包括:

  • Web 应用程序。在这种情况下,最好利用所使用的 Web 框架提供的 SQLAlchemy 集成。或者,基本模式是在 Web 请求开始时创建一个Session,在进行 POST、PUT 或 DELETE 的 Web 请求结束时调用 Session.commit() 方法,然后在 Web 请求结束时关闭该会话。通常还建议将 Session.expire_on_commit 设置为 False,以便在视图层中访问来自 Session 的对象后,如果事务已经提交,就不需要发出新的 SQL 查询来刷新对象。
  • 一个后台守护进程,会派生出子进程,希望为每个子进程创建一个Session,在子进程处理“任务”期间使用该Session,然后在任务完成时将其销毁。
  • 对于命令行脚本,应用程序将创建一个单一的全局Session,当程序开始工作时建立,并在程序完成任务时立即提交。
  • 对于 GUI 接口驱动的应用程序,Session 的范围最好在用户生成的事件范围内,例如按钮按下。或者,范围可能对应于显式用户交互,例如用户“打开”一系列记录,然后“保存”它们。

通常情况下,应用程序应该将会话的生命周期 外部 管理到处理特定数据的函数之外。这是一种将数据特定操作与它们访问和操作数据的上下文隔离开来的基本分离原则。

例如,不要这样做

### this is the **wrong way to do it** ###
class ThingOne:
    def go(self):
        session = Session()
        try:
            session.execute(update(FooBar).values(x=5))
            session.commit()
        except:
            session.rollback()
            raise
class ThingTwo:
    def go(self):
        session = Session()
        try:
            session.execute(update(Widget).values(q=18))
            session.commit()
        except:
            session.rollback()
            raise
def run_my_program():
    ThingOne().go()
    ThingTwo().go()

保持会话(通常是事务)的生命周期分离和外部。下面的示例说明了这可能的外观,并且另外利用了 Python 上下文管理器(即 with: 关键字)来自动管理Session及其事务的范围:

### this is a **better** (but not the only) way to do it ###
class ThingOne:
    def go(self, session):
        session.execute(update(FooBar).values(x=5))
class ThingTwo:
    def go(self, session):
        session.execute(update(Widget).values(q=18))
def run_my_program():
    with Session() as session:
        with session.begin():
            ThingOne().go(session)
            ThingTwo().go(session)

从 1.4 版本开始更改:Session 可以作为上下文管理器使用,无需使用外部辅助函数。

会话是一个缓存吗?

不是的。在某种程度上它被用作缓存,因为它实现了身份映射模式,并将对象键入其主键。但是,它不会做任何类型的查询缓存。这意味着,如果你说 session.scalars(select(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() 类方法,该方法在 Session 上可用:

session = Session.object_session(someobject)

较新的 运行时检查 API 系统也可以使用:

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

Session 线程安全吗?AsyncSession 在并发任务中安全吗?

Session 是一个可变的、有状态的对象,表示一个单一的数据库事务。因此,Session 的实例不能在并发线程或 asyncio 任务之间共享,除非进行仔细的同步Session 应该以非并发方式使用,也就是说,特定的 Session 实例一次只能在一个线程或任务中使用。

当使用 SQLAlchemy 的 asyncio 扩展中的AsyncSession对象时,此对象只是Session的一个薄代理,相同的规则适用;它是一个不同步的、可变的、有状态的对象,因此安全在多个 asyncio 任务中同时使用单个AsyncSession实例。

一个SessionAsyncSession实例代表一个单一的逻辑数据库事务,一次只引用一个特定EngineAsyncEngine的单一Connection,该对象绑定到其中(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然每个引擎只有一个连接在使用)。

事务中的数据库连接也是一个有状态的对象,旨在以非并发、顺序方式进行操作。命令按顺序在连接上发出,数据库服务器按照发出的顺序精确处理它们。当Session在此连接上发出命令并接收结果时,Session本身正在通过与此连接上存在的命令和数据状态一致的内部状态更改过渡;这些状态包括事务是否已开始、已提交或已回滚,是否存在任何 SAVEPOINT,以及个别数据库行的状态与本地 ORM 映射对象的状态之间的细粒度同步。

在为并发设计数据库应用程序时,适当的模型是每个并发任务/线程使用自己的数据库事务。这就是为什么在讨论数据库并发问题时,使用的标准术语是多个并发事务。在传统的 RDMS 中,没有类似于同时接收和处理多个命令的单个数据库事务的类比。

因此,SQLAlchemy 的 SessionAsyncSession 的并发模型是 每线程一个 Session,每任务一个 AsyncSession。使用多个线程或在 asyncio 中使用多个任务(例如使用 asyncio.gather() 这样的 API)的应用程序将希望确保每个线程都有其自己的 Session,每个 asyncio 任务都有其自己的 AsyncSession

确保此用法的最佳方式是在线程或任务内部的顶级 Python 函数中本地使用标准上下文管理器模式,这将确保 SessionAsyncSession 的生命周期在本地范围内保持不变。

对于从具有“全局” Session 中受益的应用程序,在不将 Session 对象传递给需要它的特定函数和方法的情况下,scoped_session 方法可以提供“线程本地” Session 对象;请参阅上下文/线程本地会话 部分了解背景。在 asyncio 上下文中,async_scoped_session 对象是 scoped_session 的 asyncio 版本,但更难配置,因为它需要自定义的“上下文”函数。


SqlAlchemy 2.0 中文文档(二十二)(4)https://developer.aliyun.com/article/1560453

相关文章
|
2天前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(二十六)(4)
SqlAlchemy 2.0 中文文档(二十六)
13 2
|
2天前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(二十二)(2)
SqlAlchemy 2.0 中文文档(二十二)
12 3
|
2天前
|
SQL 存储 数据库连接
SqlAlchemy 2.0 中文文档(二十二)(1)
SqlAlchemy 2.0 中文文档(二十二)
15 2
|
2天前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(二十二)(5)
SqlAlchemy 2.0 中文文档(二十二)
10 1
|
2天前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(二十六)(3)
SqlAlchemy 2.0 中文文档(二十六)
11 2
|
2天前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(二十六)(1)
SqlAlchemy 2.0 中文文档(二十六)
12 2
|
2天前
|
自然语言处理 数据库 Python
SqlAlchemy 2.0 中文文档(二十六)(2)
SqlAlchemy 2.0 中文文档(二十六)
12 2
|
2天前
|
SQL 前端开发 关系型数据库
SqlAlchemy 2.0 中文文档(二十七)(2)
SqlAlchemy 2.0 中文文档(二十七)
14 2
|
2天前
|
缓存 自然语言处理 数据库
SqlAlchemy 2.0 中文文档(二十六)(5)
SqlAlchemy 2.0 中文文档(二十六)
13 1
|
2天前
|
SQL 缓存 安全
SqlAlchemy 2.0 中文文档(二十二)(4)
SqlAlchemy 2.0 中文文档(二十二)
8 0