使用 ORM 进行数据作¶
上一节 使用数据 仍然从核心角度关注 SQL 表达式语言,以便在主要 SQL 语句结构之间提供连续性。然后,本节将构建 Session
的生命周期以及它如何与这些结构交互。
先决条件部分 - 本教程的 ORM 重点部分建立在本文档中之前两个以 ORM 为中心的部分之上:
使用 ORM Session 执行 - 介绍如何创建 ORMSession
对象
使用 ORM 声明式表单定义表元数据 - 我们在这里设置User
和Address
实体的 ORM 映射
选择 ORM 实体和列 - 有关如何为User
等实体运行 SELECT 语句的几个示例
使用 ORM Unit of Work 模式插入行¶
当使用 ORM 时,Session
对象负责构造 Insert
结构并将它们作为 INSERT 发出
正在进行的交易中的语句。我们指示
为此,可以通过
向其添加对象条目来执行此作;这
然后,Session
会使用称为 flush 的进程确保在需要时将这些新条目发送到数据库。Session
用于持久化对象的整个过程称为 Unit of work 模式。
类的实例表示行¶
而在前面的示例中,我们使用 Python 字典发出了 INSERT
来指示我们想要添加的数据,通过 ORM,我们直接使用
自定义 Python 类,回到
使用 ORM 声明式表单定义表元数据。在类级别,User
和
Address
类用作定义相应数据库表的外观的位置。这些类还用作可扩展数据对象,我们也使用它们来创建和作事务中的行。下面我们将创建两个 User
对象,每个对象代表一个潜在的要插入的数据库行:
>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")
我们能够在构造函数中使用映射列的名称作为关键字参数来构造这些对象。这是可能的,因为 User
类包含一个自动生成的 __init__()
构造函数,该构造函数由 ORM 映射提供,因此我们可以在构造函数中使用列名作为键来创建每个对象。
与我们的 Insert
核心示例类似,我们没有包含主键(即 id
列的条目),因为我们想利用数据库的自动递增主键功能,在本例中为 SQLite,ORM 也集成了它。如果我们要查看上述对象的 id
属性的值,则它本身显示为 None
:
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')
该值由
SQLAlchemy 提供,以指示该属性目前没有值。SQLAlchemy映射的属性在 Python 中总是返回一个值,并且在处理未分配值的新对象时,如果缺少它们,则不会引发 AttributeError
。
目前,我们上面的两个对象据说处于一种名为
transient - 它们不与任何数据库状态关联,并且尚未与可为其生成 INSERT 语句的 Session
对象关联。
向 Session 添加对象¶
为了逐步说明加法过程,我们将创建一个
Session
而不使用上下文管理器(因此我们必须确保稍后关闭它!
>>> session = Session(engine)
然后,使用
Session.add()
方法。调用此函数时,对象处于称为 pending 的状态,并且尚未插入:
>>> 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
使用一种称为 unit of work 的模式。这通常意味着它一次累积一个更改,但实际上不会在需要时将它们传达给数据库。这使它能够根据一组给定的待处理更改,更好地决定如何在事务中发出 SQL DML。当它确实向数据库发出 SQL 以推送当前更改集时,该过程称为 flush。
我们可以通过调用 Session.flush()
来手动说明 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.flush()
可用于手动推出待处理
更改当前事务,则通常没有必要,因为
Session
具有一种称为 autoflush 的行为,它
我们稍后会举例说明。 它还会在
调用 Session.commit()
时。
自动生成的主键属性¶
插入行后,我们创建的两个 Python 对象将处于称为 persistent 的状态,它们与
Session
对象,并具有许多其他行为,稍后将介绍这些行为。
发生的 INSERT 的另一个影响是 ORM 检索了每个新对象的新主键标识符;在内部,它通常使用我们之前介绍的相同 CursorResult.inserted_primary_key
Accessor。squidward
和 krabs
对象现在具有这些与之关联的新主键标识符,我们可以通过访问 id
属性来查看它们:
>>> squidward.id
4
>>> krabs.id
5
提示
为什么 ORM 本可以使用 executemany 却发出两个单独的 INSERT 语句? 正如我们将在
next 部分中,
Session
,当 flush 对象时,始终需要知道新插入对象的主键。如果使用 SQLite 的自动递增等功能(其他示例包括 PostgreSQL IDENTITY 或 SERIAL,使用序列等),该功能 CursorResult.inserted_primary_key
通常要求一次发出一行每个 INSERT。如果我们提前为主键提供值,ORM 将能够更好地优化作。一些数据库后端(例如 psycopg2)也可以一次 INSERT 多行,同时仍然能够检索主键值。
从身份映射中通过 Primary Key 获取对象¶
对象的主键标识对 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 行为和功能之前积累有关如何 SELECT 行的知识:
>>> session.commit()
COMMIT
上述作将提交正在进行的事务。我们处理过的对象仍然附加到 Session
上,这是它们一直保持的状态,直到 Session
关闭(在 关闭 Session 中介绍)。
使用 Unit of Work 模式更新 ORM 对象¶
在前面的使用 UPDATE 和 DELETE 语句中,我们介绍了
表示
SQL UPDATE 语句的 Update 结构。使用 ORM 时,有两种方式可以使用此结构。主要方法是它作为工作单元的一部分自动发出
进程,其中 UPDATE 语句是按主键发出的,对应于发生更改的单个对象。
假设我们将用户名 sandy
的 User
对象加载到一个事务中(还展示了 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
充当数据库中行的代理,更具体地说,根据
current transaction 的 current 交易,其主键身份为 2
:
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')
如果我们更改此对象的属性,则 Session
会跟踪此更改:
>>> sandy.fullname = "Sandy Squirrel"
该对象出现在名为 Session.dirty
的集合中,指示该对象是 “dirty”:
>>> sandy in session.dirty
True
当 Session
下次发出 flush 时,将发出 UPDATE
,这将更新数据库中的此值。 如前所述,flush
在我们发出任何 SELECT 之前自动发生,使用称为
autoflush 的 FLUSH 函数。我们可以直接从此行中查询 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”,因此我们稍后在回滚交易时将修复此错误。但首先,我们将进行更多数据更改。
另请参阅
Flushing- 详细说明刷新过程以及有关 Session.autoflush
设置的信息。
使用 Unit of Work 模式删除 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
标记为删除,就像其他作一样,那么在 flush 继续之前,实际上什么都不会发生:
>>> session.delete(patrick)
当前的 ORM 行为是 patrick
留在 Session
中
直到 flush 继续,如前所述,如果我们发出 query,就会发生这种情况:
>>> 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
的待处理删除正在进行中。还有一个 SELECT
针对地址
表,这是由 ORM 提示的,在此表中查找可能与目标行相关的行;此行为是称为 Cascade 的行为的一部分,可以通过允许数据库处理 Address
中的相关行来定制以更高效地工作
自然而然;删除部分 包含有关此内容的所有详细信息。
另请参阅
delete - 介绍如何优化
Session.delete()
来定义如何处理其他表中的相关行。
除此之外,现在被删除的 patrick
对象实例不再被视为在 Session
中是持久的,如包含检查所示:
>>> patrick in session
False
然而,就像我们对 sandy
对象所做的 UPDATE 一样,我们在这里所做的每一次更改都是正在进行的事务的本地更改,如果我们不提交它,它不会成为永久性的。由于目前回滚事务实际上更有趣,我们将在下一节中执行此作。
批量 / 多行 INSERT、upsert、UPDATE 和 DELETE¶
本节讨论的工作单元技术旨在将 dml 或 INSERT/UPDATE/DELETE 语句与 Python 对象机制集成,通常涉及相互关联的对象的复杂图形。使用
Session.add()
中,当创建和修改对象上的属性时,工作流单元会透明地代表我们发出 INSERT/UPDATE/DELETE。
但是,ORM 会话
还能够处理命令,允许它直接发出 INSERT、UPDATE 和 DELETE 语句,而无需传递任何 ORM 持久化对象,而是传递要 INSERT、UPDATEd 或 upserted 的值列表,或 WHERE 标准,以便可以调用一次匹配多行的 UPDATE 或 DELETE 语句。当必须影响大量行而无需构造和作 Map 对象时,这种使用模式尤为重要,这对于简单的性能密集型任务(如大型批量插入)来说可能很麻烦且没有必要。
ORM Session
的 Bulk / Multi row 功能利用了
insert()、
update()
和 delete()
结构,它们的用法类似于它们与 SQLAlchemy Core 一起使用的方式(在本教程中使用 INSERT 语句和
使用 UPDATE 和 DELETE 语句)。当这些结构与 ORM Session
而不是普通连接
一起使用时,它们的构造、执行和结果处理与 ORM 完全集成。
有关使用这些功能的背景和示例,请参阅以下部分
ORM Querying Guide 中启用了 ORM 的 INSERT、UPDATE 和 DELETE 语句。
另请参阅
回滚¶
Session
有一个 Session.rollback()
方法,该方法作为
expected 在正在进行的 SQL 连接上发出 ROLLBACK。 然而,它也
对当前与
Session
中,在前面的示例中为 Python 对象 sandy
。虽然我们将 sandy
对象的 .fullname
更改为 “Sandy
Squirrel“
,我们想回滚这个更改。 叫
Session.rollback()
不仅会回滚事务,还会
使当前与此 Session
关联的所有对象过期,这将产生这样的效果,即它们在下次访问时使用称为延迟加载的进程时刷新自己:
>>> session.rollback()
ROLLBACK
为了更仔细地查看 “expiration” 过程,我们可能会观察到 Python 对象 sandy
在其 Python __dict__
中没有留下任何状态,除了一个特殊的 SQLAlchemy 内部状态对象:
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}
这是 “expired” 状态;再次访问该属性将自动开始新事务并使用当前数据库行刷新 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'
我们现在可能会观察到,整个数据库行也被填充到
沙质
物体的__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
,这也是我们在上下文管理器中使用它时发生的情况,可以完成以下作:
它将所有连接资源释放到连接池中,取消(例如回滚)任何正在进行的事务。
这意味着,当我们使用会话来执行一些只读 tasks 然后关闭它,我们不需要显式调用Session.rollback()
来确保事务回滚;连接池处理此问题。
它会从Session
中清除所有对象。
这意味着我们为此Session
加载的所有 Python 对象(如sandy
、patrick
和squidward
)现在都处于称为 detached 的状态。特别是,我们将注意到仍处于过期状态的对象(例如由于调用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
使用Session.add()
方法,该方法将重新建立它们与特定数据库行的关系:>>> 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'
提示
如果可能,请尽量避免使用处于 detached 状态的对象。当Session
已关闭,则还会清理对以前附加的所有对象的引用。对于需要分离对象的情况,通常是在呈现视图之前关闭Session
的 Web 应用程序的立即显示刚刚提交的对象,请将Session.expire_on_commit
flag 设置为False
。