使用 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')

SQLAlchemy 提供了 None 值来表示该属性目前没有值。SQLAlchemy 映射属性始终在 Python 中返回一个值,并且在处理没有分配值的全新对象时,不会引发 AttributeError

目前,我们上面的两个对象被称为处于 瞬态 状态 - 它们没有与任何数据库状态关联,并且尚未与可以为它们生成 INSERT 语句的 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 会发出两个单独的 INSERT 语句,而它可以使用 executemany?正如我们将在下一节中看到的那样,Session 在刷新对象时总是需要知道新插入对象的 主键。如果使用 SQLite 的自动增量之类的功能(其他示例包括 PostgreSQL IDENTITY 或 SERIAL,使用序列等),CursorResult.inserted_primary_key 功能通常要求每次发出一个 INSERT 语句。如果我们提前提供了主键值,则 ORM 能够更好地优化操作。某些数据库后端(例如 psycopg2)也可以一次插入多行,同时仍然能够检索 主键值。

从身份映射中获取 主键的对象

对象的 主键身份对 Session 非常重要,因为现在这些对象使用称为 身份映射 的功能在内存中与该身份相关联。身份映射是一个内存存储,它将当前在内存中加载的所有对象与其 主键身份相关联。我们可以使用 Session.get() 方法来检索上述对象之一,如果本地存在,它将返回身份映射中的条目,否则会发出 SELECT

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

关于身份映射要注意的重要一点是,它在特定 Session 对象的范围内,为每个特定数据库身份维护特定 Python 对象的 **唯一实例**。我们可能会观察到 some_squidward 指的是与之前 squidward 相同的 **对象**

>>> some_squidward is squidward
True

身份映射是一个关键功能,它允许在事务内操作复杂的对象集,而不会出现不同步的情况。

提交

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

>>> session.commit()
COMMIT

以上操作将提交正在进行的事务。我们已经处理过的对象仍然与 Session 相关联,它们处于这种状态,直到 Session 关闭(在 关闭会话 中介绍)。

提示

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

使用工作单元模式更新 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,它会更新数据库中的此值。如前所述,在发出任何 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,这是刷新过程将挂起的更改推出的过程。Python 对象 sandy 现在不再被视为脏的

>>> sandy in session.dirty
False

但是请注意,我们 **仍然处于事务中**,并且我们的更改尚未推送到数据库的永久存储中。由于桑迪的姓氏实际上是“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 中的相关行来调整效率;部分 删除 对此进行了详细说明。

另请参阅

删除 - 描述了如何调整 Session.delete() 的行为,具体是指如何处理其他表中的相关行。

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

>>> patrick in session
False

但是,就像我们对 sandy 对象进行的 UPDATE 一样,我们在这里进行的每一次更改都只针对正在进行的事务,如果我们不提交它,这些更改将不会永久生效。由于目前回滚事务更有趣,我们将在下一节中执行此操作。

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

本节讨论的 工作单元 技术旨在将 DML(即 INSERT/UPDATE/DELETE 语句)与 Python 对象机制集成在一起,通常涉及相互关联对象的复杂图形。将对象添加到 Session(使用 Session.add())后,工作单元流程会透明地发出 INSERT/UPDATE/DELETE,这些语句代表我们在对象上创建和修改属性时进行操作。

但是,ORM Session 还可以处理命令,这些命令允许它直接发出 INSERT、UPDATE 和 DELETE 语句,而无需传入任何 ORM 持久化的对象,而是传入要插入、更新或 upsert 的值列表,或 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