使用 ORM 进行数据操作

上一节使用数据仍然侧重于从 Core 角度的 SQL 表达式语言,以便在主要的 SQL 语句构造中提供连续性。本节将构建Session的生命周期,以及它如何与这些构造交互。

先决条件章节 - 本教程中以 ORM 为重点的部分建立在本文档中之前的两个以 ORM 为中心的章节之上

使用 ORM 工作单元模式插入行

当使用 ORM 时,Session 对象负责构造 Insert 构造并在正在进行的事务中发出它们作为 INSERT 语句。我们指示 Session 这样做的方式是添加对象条目到它;Session 然后确保这些新条目将在需要时使用称为刷新的过程发出到数据库。 Session 用于持久化对象的整个过程称为工作单元模式。

类的实例代表行

在之前的示例中,我们使用 Python 字典发出 INSERT 以指示我们要添加的数据,而在 ORM 中,我们直接使用我们在 使用 ORM 声明式表单定义表元数据 中定义的自定义 Python 类。在类级别,UserAddress 类充当定义相应数据库表外观的位置。这些类也充当可扩展的数据对象,我们使用它们来创建和操作事务中的行。下面我们将创建两个 User 对象,每个对象代表一个要 INSERT 的潜在数据库行

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

我们能够使用映射列的名称作为构造函数中的关键字参数来构造这些对象。这是可能的,因为 User 类包含一个由 ORM 映射自动生成的 __init__() 构造函数,以便我们可以使用列名作为构造函数中的键来创建每个对象。

与我们在 Insert 的 Core 示例中类似,我们没有包含主键(即 id 列的条目),因为我们想利用数据库的自动递增主键功能,在本例中为 SQLite,ORM 也与之集成。如果我们查看上面对象的 id 属性的值,它会显示为 None

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

None 值由 SQLAlchemy 提供,以指示该属性目前没有值。 SQLAlchemy 映射的属性始终在 Python 中返回值,并且在处理尚未赋值的新对象时,不会引发 AttributeError

目前,我们上面的两个对象据说处于称为瞬态的状态 - 它们不与任何数据库状态关联,并且尚未与可以为它们生成 INSERT 语句的 Session 对象关联。

向 Session 添加对象

为了逐步说明添加过程,我们将创建一个不使用上下文管理器的 Session(因此我们必须确保稍后关闭它!)

>>> session = Session(engine)

然后使用 Session.add() 方法将对象添加到 Session。调用此方法时,对象处于称为挂起的状态,并且尚未插入

>>> session.add(squidward)
>>> session.add(krabs)

当我们有挂起对象时,我们可以通过查看 Session 上名为 Session.new 的集合来查看此状态

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

上面的视图正在使用一个名为 IdentitySet 的集合,它本质上是一个 Python 集合,在所有情况下都基于对象标识进行哈希(即,使用 Python 内置的 id() 函数,而不是 Python hash() 函数)。

刷新

Session 使用一种称为工作单元的模式。 这通常意味着它一次累积一个更改,但直到需要时才真正将其传达给数据库。 这使其能够根据给定的一组挂起更改,更好地决定如何在事务中发出 SQL DML。 当它确实将 SQL 发送到数据库以推送当前的一组更改时,该过程称为刷新

我们可以通过调用 Session.flush() 方法手动说明刷新过程

>>> session.flush()
BEGIN (implicit) INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('squidward', 'Squidward Tentacles') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [insertmanyvalues 2/2 (ordered; batch not supported)] ('ehkrabs', 'Eugene H. Krabs')

上面我们观察到首先调用 Session 来发出 SQL,因此它创建了一个新事务并为这两个对象发出了适当的 INSERT 语句。 事务现在保持打开状态,直到我们调用 Session.commit(), Session.rollback(), 或 Session.close() 方法中的任何一个 Session

虽然 Session.flush() 可用于手动将挂起的更改推送到当前事务,但通常是不必要的,因为 Session 具有称为 自动刷新 的行为,我们稍后将对此进行说明。 它还在调用 Session.commit() 时刷新更改。

自动生成的主键属性

一旦插入行,我们创建的两个 Python 对象就处于称为持久化的状态,它们与添加或加载它们的 Session 对象关联,并具有许多其他将在以后介绍的行为。

发生的 INSERT 的另一个影响是 ORM 检索了每个新对象的新主键标识符; 在内部,它通常使用我们之前介绍的相同的 CursorResult.inserted_primary_key 访问器。 squidwardkrabs 对象现在具有与它们关联的这些新的主键标识符,我们可以通过访问 id 属性来查看它们

>>> squidward.id
4
>>> krabs.id
5

提示

当 ORM 可以使用 executemany 时,为什么它发出两个单独的 INSERT 语句? 正如我们将在下一节中看到的,Session 在刷新对象时总是需要知道新插入对象的主键。 如果使用诸如 SQLite 的自动增量之类的功能(其他示例包括 PostgreSQL IDENTITY 或 SERIAL,使用序列等),CursorResult.inserted_primary_key 功能通常要求每次 INSERT 发出一行。 如果我们提前为主键提供了值,ORM 将能够更好地优化操作。 某些数据库后端(例如 psycopg2)也可以一次 INSERT 多行,同时仍然能够检索主键值。

从 Identity Map 中按主键获取对象

对象的主键标识对于 Session 非常重要,因为对象现在使用称为identity map的功能在内存中链接到此标识。 Identity map 是一个内存中存储,它将当前加载到内存中的所有对象链接到它们的主键标识。 我们可以通过使用 Session.get() 方法检索上述对象之一来观察这一点,如果本地存在,它将从 identity map 返回一个条目,否则发出一个 SELECT

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

关于 identity map 需要注意的重要一点是,它在特定 Session 对象的范围内,为每个特定的数据库标识维护一个特定的 Python 对象的唯一实例。 我们可以观察到 some_squidward 指的是与之前的 squidward 相同的对象

>>> some_squidward is squidward
True

identity map 是一项关键功能,它允许在事务中操作复杂的对象集,而不会出现不同步的情况。

提交

关于 Session 的工作原理还有很多要说的,我们将在后面进一步讨论。 现在我们将提交事务,以便我们可以构建关于如何在检查更多 ORM 行为和功能之前 SELECT 行的知识

>>> session.commit()
COMMIT

上述操作将提交正在进行的事务。 我们处理过的对象仍然附加Session,这是它们保持的状态,直到 Session 关闭(在 关闭 Session 中介绍)。

提示

需要注意的重要一点是,我们刚刚处理的对象的属性已过期,这意味着,当我们下次访问它们的任何属性时,Session 将启动一个新事务并重新加载它们的状态。 对于性能原因,或者如果希望在关闭 Session(称为分离状态)后使用对象,此选项有时会存在问题,因为它们将没有任何状态,并且没有 Session 可以加载该状态,从而导致“分离实例”错误。 可以使用名为 Session.expire_on_commit 的参数来控制此行为。 有关此内容的更多信息,请参见 关闭 Session

使用工作单元模式更新 ORM 对象

在上一节 使用 UPDATE 和 DELETE 语句 中,我们介绍了 Update 构造,它代表 SQL UPDATE 语句。 当使用 ORM 时,有两种使用此构造的方式。 主要方式是作为 工作单元 过程的一部分自动发出,该过程由 Session 使用,其中针对具有更改的各个对象,在每个主键的基础上发出 UPDATE 语句。

假设我们将用户名为 sandyUser 对象加载到事务中(还展示了 Select.filter_by() 方法以及 Result.scalar_one() 方法)

>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('sandy',)

Python 对象 sandy 如前所述充当数据库中行的代理,更具体地说是当前事务中的数据库行,其主键标识为 2

>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

如果我们更改此对象的属性,Session 会跟踪此更改

>>> sandy.fullname = "Sandy Squirrel"

该对象出现在名为 Session.dirty 的集合中,指示该对象为“脏”

>>> sandy in session.dirty
True

Session 下次发出刷新时,将发出一个 UPDATE,该 UPDATE 将更新数据库中的此值。 如前所述,刷新会在我们发出任何 SELECT 之前自动发生,使用称为 自动刷新 的行为。 我们可以直接从该行查询 User.fullname 列,我们将获得更新后的值

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
UPDATE user_account SET fullname=? WHERE user_account.id = ? [...] ('Sandy Squirrel', 2) SELECT user_account.fullname FROM user_account WHERE user_account.id = ? [...] (2,)
>>> print(sandy_fullname) Sandy Squirrel

我们可以在上面看到,我们请求 Session 执行单个 select() 语句。 但是,发出的 SQL 显示也发出了 UPDATE,这是刷新过程推送挂起的更改。 sandy Python 对象现在不再被认为是脏的

>>> sandy in session.dirty
False

但是请注意,我们仍然在一个事务中,并且我们的更改尚未推送到数据库的永久存储中。 由于 Sandy 的姓氏实际上是“Cheeks”而不是“Squirrel”,我们稍后将在回滚事务时修复此错误。 但首先我们将进行更多数据更改。

另请参阅

刷新- 详细介绍了刷新过程以及有关 Session.autoflush 设置的信息。

使用工作单元模式删除 ORM 对象

为了完善基本持久化操作,可以使用 Session.delete() 方法在工作单元过程中将单个 ORM 对象标记为删除。 让我们从数据库中加载 patrick

>>> patrick = session.get(User, 3)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname FROM user_account WHERE user_account.id = ? [...] (3,)

如果我们将 patrick 标记为删除,与其他操作一样,在刷新进行之前实际上什么都不会发生

>>> session.delete(patrick)

当前的 ORM 行为是 patrick 停留在 Session 中,直到刷新进行,如前所述,如果我们发出查询,则会发生刷新

>>> session.execute(select(User).where(User.name == "patrick")).first()
SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id FROM address WHERE ? = address.user_id [...] (3,) DELETE FROM user_account WHERE user_account.id = ? [...] (3,) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('patrick',)

上面,我们请求发出的 SELECT 之前有一个 DELETE,这表明 patrick 的挂起删除已进行。 还有一个针对 address 表的 SELECT,这是由 ORM 查找此表中可能与目标行相关的行而引起的; 此行为是称为级联的行为的一部分,可以通过允许数据库自动处理 address 中的相关行来更有效地进行调整; delete 部分包含有关此的所有详细信息。

另请参阅

delete - 描述了如何调整 Session.delete() 在如何处理其他表中相关行方面的行为。

除此之外,现在被删除的 patrick 对象实例不再被认为在 Session 中是持久的,如包含检查所示

>>> patrick in session
False

但是,就像我们对 sandy 对象所做的 UPDATE 一样,我们在这里所做的每个更改都仅限于正在进行的事务,如果我们不提交它,这些更改将不会变为永久性的。 由于回滚事务实际上目前更有趣,我们将在下一节中执行此操作。

批量/多行 INSERT、upsert、UPDATE 和 DELETE

本节中讨论的工作单元技术旨在将 dml 或 INSERT/UPDATE/DELETE 语句与 Python 对象机制集成,通常涉及复杂的相互关联的对象图。 一旦使用 Session.add() 将对象添加到 Session,工作单元过程就会在我们对象的属性被创建和修改时,透明地代表我们发出 INSERT/UPDATE/DELETE。

然而,ORM Session 也具有处理命令的能力,这些命令允许它直接发出 INSERT、UPDATE 和 DELETE 语句,而无需传递任何 ORM 持久化对象,而是传递要插入、更新或更新插入的值列表,或 WHERE 条件,以便可以一次调用与多行匹配的 UPDATE 或 DELETE 语句。当必须影响大量行,而无需构建和操作映射对象时,这种使用模式尤其重要,对于简单的、性能密集型任务(例如大型批量插入)而言,构建和操作映射对象可能既繁琐又没有必要。

ORM Session 的批量/多行功能使用了 insert()update()delete() 构造,并且它们的用法类似于它们在 SQLAlchemy Core 中的使用方式(在本教程中首次在 使用 INSERT 语句使用 UPDATE 和 DELETE 语句 中介绍)。当在 ORM Session 而不是普通的 Connection 中使用这些构造时,它们的构造、执行和结果处理与 ORM 完全集成。

有关使用这些功能的背景和示例,请参阅 ORM 支持的 INSERT、UPDATE 和 DELETE 语句 部分,该部分位于 ORM 查询指南 中。

回滚

Session 具有 Session.rollback() 方法,正如预期的那样,该方法在正在进行的 SQL 连接上发出 ROLLBACK。但是,它也会对当前与 Session 关联的对象产生影响,在我们之前的示例中,Python 对象 sandy 就是如此。虽然我们将 sandy 对象的 .fullname 更改为 "Sandy Squirrel",但我们想回滚此更改。调用 Session.rollback() 不仅会回滚事务,还会使当前与此 Session 关联的所有对象过期,这将导致它们在下次访问时使用称为延迟加载 的过程自行刷新

>>> session.rollback()
ROLLBACK

要更仔细地查看“过期”过程,我们可以观察到 Python 对象 sandy 的 Python __dict__ 中没有留下任何状态,除了一个特殊的 SQLAlchemy 内部状态对象

>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

这就是“已过期”状态;再次访问该属性将自动开始一个新的事务,并使用当前的数据库行刷新 sandy

>>> sandy.fullname
BEGIN (implicit) SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname FROM user_account WHERE user_account.id = ? [...] (2,)
'Sandy Cheeks'

我们现在可以观察到完整的数据库行也被填充到 sandy 对象的 __dict__

>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

对于已删除的对象,当我们之前注意到 patrick 不再会话中时,该对象的标识也会恢复

>>> patrick in session
True

当然,数据库数据也再次出现

>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('patrick',)
True

关闭会话

在以上部分中,我们在 Python 上下文管理器之外使用了 Session 对象,也就是说,我们没有使用 with 语句。这没关系,但是如果我们以这种方式做事,最好在我们完成 Session 后显式关闭它

>>> session.close()
ROLLBACK

关闭 Session(当我们也在上下文管理器中使用它时也会发生这种情况)完成了以下事情

  • 释放所有连接资源到连接池,取消(例如,回滚)任何正在进行的事务。

    这意味着当我们使用会话执行一些只读任务,然后关闭它时,我们不需要显式调用 Session.rollback() 以确保事务被回滚;连接池会处理这个问题。

  • 它从 Session驱逐所有对象。

    这意味着我们为此 Session 加载的所有 Python 对象,如 sandypatricksquidward,现在都处于称为已分离的状态。特别是,我们会注意到,仍然处于已过期状态的对象(例如,由于调用 Session.commit()),现在是无法使用的,因为它们不包含当前行的状态,并且不再与任何数据库事务关联,因此无法刷新

    # note that 'squidward.name' was just expired previously, so its value is unloaded
    >>> squidward.name
    Traceback (most recent call last):
      ...
    sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed

    可以使用 Session.add() 方法将分离的对象与相同或新的 Session 重新关联,这将重新建立它们与其特定数据库行的关系

    >>> session.add(squidward)
    >>> squidward.name
    
    BEGIN (implicit) SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname FROM user_account WHERE user_account.id = ? [...] (4,)
    'squidward'

    提示

    如果可能,请尽量避免在其分离状态下使用对象。当 Session 关闭时,也要清理对所有先前附加对象的引用。对于需要分离对象的情况,通常是在 Web 应用程序中立即显示刚刚提交的对象,其中在渲染视图之前关闭了 Session,请将 Session.expire_on_commit 标志设置为 False