使用 ORM 相关对象¶
在本节中,我们将介绍一个更基本的 ORM 概念,即 ORM 如何与引用其他对象的映射类进行交互。在 声明映射类 一节中,映射类示例使用了名为 relationship() 的
构造。此构造定义两个不同映射类之间的链接,或从映射类到自身的链接,后者称为自引用关系。
为了描述 relationship()
的基本思想,首先我们将以简短的形式回顾 Map,省略 mapped_column()
映射和其他指令:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
# ... mapped_column() mappings
addresses: Mapped[List["Address"]] = relationship(back_populates="user")
class Address(Base):
__tablename__ = "address"
# ... mapped_column() mappings
user: Mapped["User"] = relationship(back_populates="addresses")
在上面,User
类现在有一个属性 User.addresses
,而
Address
类具有属性 Address.user
。 这
relationship()
结构与
Mapped
构造来指示键入行为,将用于检查映射到 User
和 Address
类的 Table
对象之间的表关系。由于
Table
对象具有
ForeignKeyConstraint
调用user_account
table 中,relationship()
可以明确地确定从 User
类到 Address
存在一对多的关系
类,沿 User.addresses
关系;其中一行
user_account
表中的多个行可能引用
桌子。
所有一对多关系自然都对应于多对一
关系,在本例中为
上面
看到的 relationship.back_populates
参数在引用另一个名称的两个 relationship()
对象上配置,它建立这两个 relationship()
中的每一个
结构应被视为彼此互补;我们拭目以待
这在下一节中将如何发挥作用。
持久化和加载关系¶
我们可以从说明 relationship()
对对象实例的作用开始。如果我们创建一个新的 User
对象,我们可以注意到当我们访问 .addresses
元素时,有一个 Python 列表:
>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]
此对象是 Python 列表
的 SQLAlchemy 特定版本,能够跟踪和响应对其所做的更改。当我们访问该属性时,该集合也会自动显示,即使我们从未将其分配给该对象。这类似于使用 ORM Unit of Work 模式插入行中提到的行为,在该模式中观察到我们没有明确分配值的基于列的属性也会自动显示为 None
,而不是像 Python 的通常行为那样引发 AttributeError
。
由于 u1
对象仍然是瞬态的,并且我们从 u1.addresses
得到的列表
还没有被改变(即附加或扩展),它实际上还没有与对象关联,但当我们对其进行更改时,它将成为 User
对象状态的一部分。
该集合特定于 Address
类,该类是可以在其中持久保存的唯一 Python 对象类型。使用 list.append()
方法中,我们可以添加一个 Address
对象:
>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)
此时,u1.addresses
集合如预期包含新的 Address
对象:
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]
当我们将 Address
对象与 u1
实例的 User.addresses
集合关联时,还发生了另一种行为,即
User.addresses
关系与 Address.user
同步
关系,这样我们不仅可以从 User
对象导航到 Address
对象,还可以从 Address
对象导航回 “父” User
对象:
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')
这种同步是由于我们使用了
relationship.back_populates
参数
relationship()
对象。 此参数将另一个
relationship()
应该发生互补属性赋值/列表突变。它在另一个方向上同样有效,即如果我们创建另一个 Address
对象并分配给其 Address.user
属性,则该 Address
将成为
User.addresses
集合中:
>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]
我们实际上在
Address
构造函数,就像在 Address
类上声明的任何其他映射属性一样,该构造函数被接受。它相当于事后分配 Address.user
属性:
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
将对象级联到 Session 中¶
现在,我们在内存中有一个 User
和两个 Address
对象,它们在内存中的双向结构中关联,但正如前面的 Inserting Rows using the ORM Unit of Work pattern 中所述,这些对象在与 Session
对象关联之前被称为瞬态状态。
我们使用了仍在进行的 Session
,并注意到,当我们将 Session.add()
方法应用于 lead User
对象时,相关的 Address
对象也会被添加到同一个 Session
中:
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True
上述行为中,Session
接收到一个 User
对象,并沿着 User.addresses
关系查找相关的
Address
对象称为 save-update cascade,在 Cascades 的 ORM 参考文档中有详细讨论。
这三个对象现在处于 pending 状态;这意味着它们已准备好成为 INSERT作的主题,但这尚未进行;这三个对象都尚未分配主键,此外,A1
和 A2
对象有一个名为 user_id
的属性,该属性引用
具有 ForeignKeyConstraint
的列
引用 user_account.id
列;这些也是 None
,因为对象尚未与实际的数据库行关联:
>>> print(u1.id)
None
>>> print(a1.user_id)
None
正是在这个阶段,我们可以看到 unit of work process 提供的非常大的效用;回想一下,在 INSERT 节中,通常会自动生成 “values” 子句,行入到user_account
中,并且
地址
表,以便自动将 address.user_id
列与 user_account
的列相关联
行。 此外,我们还必须为 user_account
行在行
之前,因为 address
中的行是
依赖于它们在 user_account
中的父行,以获取其
user_id
列。
使用 Session
时,所有这些乏味的事情都为我们处理,即使是最顽固的 SQL 纯粹主义者也可以从 INSERT、UPDATE 和 DELETE 语句的自动化中受益。当我们 Session.commit()
事务所有步骤都以正确的顺序调用时,此外,user_account
行新生成的主键将应用于
适当地address.user_id
列:
>>> session.commit()
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('pkrabs', 'Pearl Krabs')
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('pearl.krabs@gmail.com', 6)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('pearl@aol.com', 6)
COMMIT
加载关系¶
在最后一步中,我们调用了 Session.commit(),
它发出了一个 COMMIT
对于交易,然后按
Session.commit.expire_on_commit
过期所有对象,以便它们在下一个事务中刷新。
当我们下次访问这些对象上的属性时,我们将看到为该行的主属性发出的 SELECT,例如当我们查看为 u1
对象新生成的主键时:
>>> u1.id
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 = ?
[...] (6,)
6
u1
User
对象现在有一个持久化集合 User.addresses
我们也可以访问。 由于此集合由一组附加
的行,当我们访问这个集合时,我们再次看到为了检索对象而发出的延迟加载:
>>> u1.addresses
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
[...] (6,)
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
SQLAlchemy ORM 中的集合和相关属性在内存中是持久的;填充集合或属性后,在该集合或属性过期之前,不再发出 SQL。 我们可能会访问
u1.addresses
以及添加或删除项目,这不会产生任何新的 SQL 调用:
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
虽然延迟加载发出的加载很快就会变得昂贵,但如果
我们没有采取明确的步骤来优化它,对网络的延迟加载
至少经过了相当好的优化,不会执行冗余工作;作为
u1.addresses
集合已刷新,根据身份映射
这些实际上是相同的
Address
实例作为我们已经处理过的 a1
和 a2
对象,所以我们已经完成了这个特定对象图中的所有属性的加载:
>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')
关系如何加载的问题,或者是否加载,是一个完整的主题。本节后面的 Loader 策略中将对这些概念进行一些其他介绍。
在查询中使用关系¶
上一节介绍了 relationship()
的行为
构造,在上面,使用 Map 类的实例时,
User
和 Address
类的 u1
、a1
和 a2
实例。在本节中,我们将介绍 relationship()
的行为,因为它适用于映射类的类级行为,它以多种方式帮助自动化 SQL 查询的构造。
使用关系进行连接¶
显式 FROM 子句和 JOIN 以及
设置 ON 子句引入了
Select.join()
和 Select.join_from()
方法来编写 SQL JOIN 子句。为了描述如何在表之间进行连接,这些方法要么根据链接两个表的表元数据结构中存在单个明确的 ForeignKeyConstraint
对象来推断 ON 子句,要么我们可以提供一个显式的 SQL 表达式结构来指示特定的 ON 子句。
当使用 ORM 实体时,可以使用一种额外的机制来帮助我们设置 join 的 ON 子句,即使用 relationship()
对象,如
声明映射类。与 relationship()
对应的类绑定属性可以作为单个
参数添加到 Select.join()
中,它用于同时指示 join 的右侧以及 ON 子句:
>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
Select.join()
或 Select.join_from()
不使用映射上存在的 ORM relationship()
推断 ON 子句,如果我们不这样做
指定它。 这意味着,如果我们在没有 ON 子句的情况下从 User
连接到 Address
,它之所以有效,是因为 ForeignKeyConstraint
在两个映射的 Table
对象之间,而不是因为
relationship()
对象:
>>> print(select(Address.email_address).join_from(User, Address))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
请参阅 ORM Querying Guide 中的 Joins 部分
有关如何使用 Select.join()
和 Select.join_from()
的更多示例
with relationship()
结构。
另请参阅
关系 WHERE 运算符¶
还有一些其他种类的 SQL 生成帮助程序
relationship()
的 API 在构建
WHERE 子句。 请参阅该部分
ORM Querying Guide 中的 RELATIONSHIP WHERE 运算符。
另请参阅
Loader 策略¶
在 加载关系 一节中,我们引入了这样一个概念:当我们使用映射对象的实例时,默认情况下访问 relationship()
映射的属性将在集合未填充时发出延迟加载,以便加载应该存在于此集合中的对象。
延迟加载是最著名的 ORM 模式之一,也是
最具争议。 当内存中的几十个 ORM 对象分别引用
少量未加载的属性,则对这些对象的常规作可以
分拆出许多其他查询,这些查询可以加起来(也称为
N 加 1 个问题),更糟糕的是,它们是隐式发出的。这些隐式查询可能不会被注意到,当在不再有可用的数据库事务后尝试时,或者当使用替代并发模式(如 asyncio)时,它们实际上根本不起作用。
同时,当延迟加载与正在使用的并发方法兼容并且不会引起问题时,延迟加载是一种非常流行且有用的模式。由于这些原因,SQLAlchemy 的 ORM 非常重视能够控制和优化这种加载行为。
最重要的是,有效使用 ORM 延迟加载的第一步是测试
应用程序,打开 SQL 回显,并观察发出的 SQL。如果看起来有很多冗余的 SELECT 语句看起来非常像可以更有效地合并为一个,如果对于已从 Session
分离的对象有不适当的加载,那么就是考虑使用 loader 的时候
策略。
Loader 策略表示为可以使用 Select.options()
方法与 SELECT 语句关联的对象,例如:
for user_obj in session.execute(
select(User).options(selectinload(User.addresses))
).scalars():
user_obj.addresses # access addresses collection already loaded
它们也可以使用 relationship.lazy
选项配置为 relationship()
的默认值,例如:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", lazy="selectin"
)
每个 loader 策略对象都会向语句中添加某种信息,稍后 Session
在决定如何加载和/或在访问它们时的行为时将使用这些信息。
以下部分将介绍一些最常用的 loader 策略。
另请参阅
关系加载技术中的两个部分:
在 Mapping 时配置 Loader 策略 - 有关在relationship()
上配置策略的详细信息
使用 Loader 选项进行关系加载 - 有关使用查询时加载器策略的详细信息
Selectin Load¶
现代 SQLAlchemy 中最有用的加载器是
selectInload()
加载器选项。此选项解决了“N 加 1”问题的最常见形式,即引用相关集合的一组对象。selectInload()
将确保使用单个查询预先加载完整系列对象的特定集合。它使用 SELECT 表单来实现此目的,在大多数情况下,该表单可以单独针对相关表发出,而无需引入 JOIN 或子查询,并且仅查询尚未加载集合的父对象。下面我们说明 selectinload()
通过加载所有 User
对象及其所有相关的 Address
对象;虽然我们只调用了一次 Session.execute(),
但给定一个
select()
结构,当访问数据库时,实际上会发出两个 SELECT 语句,第二个是获取相关的 Address
对象:
>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
... print(
... f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})"
... )
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account ORDER BY user_account.id
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id,
address.email_address AS address_email_address
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
spongebob (spongebob@sqlalchemy.org)
sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick ()
squidward ()
ehkrabs ()
pkrabs (pearl.krabs@gmail.com, pearl@aol.com)
联合负载¶
joinedload()
预先加载策略是 SQLAlchemy 中最古老的预先加载器,它使用 JOIN(可能是外部或内部连接,具体取决于选项)来增强传递给数据库的 SELECT 语句,然后可以加载相关对象。
joinedload()
策略最适合加载相关的多对一对象,因为这只需要将额外的列添加到在任何情况下都会获取的主实体行中。为了提高效率,它还接受选项 joinedload.innerjoin
因此,对于以下情况,可以使用 inner join 而不是 outer join。
如下所示,其中我们知道所有 Address
对象都有一个关联的
用户
:
>>> from sqlalchemy.orm import joinedload
>>> stmt = (
... select(Address)
... .options(joinedload(Address.user, innerjoin=True))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1,
user_account_1.name, user_account_1.fullname
FROM address
JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
ORDER BY address.id
[...] ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
joinedload()
也适用于集合,即一对多关系,但是它具有以递归方式将每个相关项目的主行相乘的效果,对于嵌套集合和/或更大的集合,为结果集发送的数据量增加几个数量级,因此它与其他选项(如 selectInload()
)的使用应根据具体情况进行评估。
请务必注意,封闭的 WHERE 和 ORDER BY 条件
select
语句不以
joinedload() 的在上面,可以在 SQL 中看到,匿名别名
应用于 user_account
表,使其不可直接寻址
在查询中。 本节将更详细地讨论此概念
加入 Eager Loading 的禅意。
提示
需要注意的是,多对一的预先加载通常不是必需的,因为 “N 加 1” 问题在常见情况下要少得多。当许多对象都引用同一个相关对象时,例如许多 Address
对象,则使用正常的延迟加载为该
User
对象仅发出一次 SQL。 延迟加载例程
会按照当前
Session
时,尽可能不发出任何 SQL。
显式 Join + Eager load¶
如果我们在使用 Select.join()
等方法连接到 user_account
表时加载 Address
行来呈现 JOIN,我们可以
还要利用该 JOIN 来预先加载
Address.user
属性。 这是
本质上,我们使用了 “joined eager loading”,但渲染了 JOIN
我们自己。 此常见用例是通过使用
contains_eager()
选项。此选项与
joinedload()
中,不同之处在于它假定我们已经自己设置了 JOIN,而它只指示 COLUMNS 子句中的其他列应该加载到每个返回对象的相关属性中,例如:
>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(contains_eager(Address.user))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT user_account.id, user_account.name, user_account.fullname,
address.id AS id_1, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? ORDER BY address.id
[...] ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
在上面,我们既过滤了 user_account.name
上的行,又将 user_account
中的行加载到返回行的 Address.user
属性中。如果我们单独应用 joinedload(),
我们将得到一个不必要地连接两次的 SQL 查询:
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(joinedload(Address.user))
... .order_by(Address.id)
... )
>>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT address.id, address.email_address, address.user_id,
user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname
FROM address JOIN user_account ON user_account.id = address.user_id
LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
WHERE user_account.name = :name_1 ORDER BY address.id
提高负载¶
另一个值得一提的 loader 策略是 raiseload()。
此选项用于完全阻止应用程序具有
N 加上一个问题,导致通常的延迟加载引发错误。它有两个变体,通过 raiseload.sql_only
选项进行控制,以阻止需要 SQL 的延迟加载,而不是所有 “加载”作,包括那些只需要咨询当前 Session
的作。
使用 raiseload()
的一种方法是在
relationship()
本身,通过设置 relationship.lazy
设置为值 “raise_on_sql”
,因此对于特定的 Map,特定关系将永远不会尝试发出 SQL:
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship
>>> class User(Base):
... __tablename__ = "user_account"
... id: Mapped[int] = mapped_column(primary_key=True)
... addresses: Mapped[List["Address"]] = relationship(
... back_populates="user", lazy="raise_on_sql"
... )
>>> class Address(Base):
... __tablename__ = "address"
... id: Mapped[int] = mapped_column(primary_key=True)
... user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
... user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")
使用这样的 Map,应用程序被阻止延迟加载,这表明特定查询需要指定加载器策略:
>>> u1 = session.execute(select(User)).scalars().first()
SELECT user_account.id FROM user_account
[...] ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'
该异常将指示此集合应预先加载:
>>> u1 = (
... session.execute(select(User).options(selectinload(User.addresses)))
... .scalars()
... .first()
... )
SELECT user_account.id
FROM user_account
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
lazy=“raise_on_sql”
选项也尝试对多对一关系保持智能;上面,如果
Address
对象未加载,但该 User
对象本地存在于同一个 Session
中,则 “raiseload” 策略不会引发错误。