SQLAlchemy 2.0 文档
- 上一页: 使用 ORM 进行数据操作
- 下一页: 进一步阅读
- 上一级: 首页
- 本页内容
使用 ORM 相关对象¶
在本节中,我们将介绍另一个重要的 ORM 概念,即 ORM 如何与映射到其他对象的映射类进行交互。在 声明映射类 部分,映射类示例使用了名为 relationship()
的构造函数。此构造函数定义了两个不同映射类之间或映射类与自身之间的连接,后者称为 **自引用** 关系。
为了描述 relationship()
的基本概念,我们将简要回顾一下映射,省略 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
对象之间的表关系。由于表示 address
表的 Table
对象有一个 ForeignKeyConstraint
指向 user_account
表,relationship()
可以明确地确定 User
类到 Address
类,沿着 User.addresses
关系存在着 一对多 关系;user_account
表中的一行可能被 address
表中的多行引用。
所有一对多关系自然地对应着另一个方向的 多对一 关系,在本例中,它由 Address.user
指示。 relationship.back_populates
参数,如上面所示,在两个引用另一个名称的 relationship()
对象上配置,表明这两个 relationship()
构造函数应该被认为是彼此互补的;我们将在下一节中看到它的实际应用。
持久化和加载关系¶
我们可以从说明 relationship()
对对象实例的影响开始。如果我们创建一个新的 User
对象,我们可以注意到,当访问 .addresses
元素时,会出现一个 Python 列表
>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]
此对象是 SQLAlchemy 特定的 Python list
版本,它能够跟踪并响应对其进行的更改。即使我们从未将集合分配给对象,它也自动出现。这类似于在 使用 ORM 工作单元模式插入行 中观察到的行为,其中观察到,我们没有显式分配值的基于列的属性也自动显示为 None
,而不是像 Python 通常的行为那样引发 AttributeError
。
由于 u1
对象仍然是 瞬态 的,我们从 u1.addresses
获得的 list
尚未被修改(即附加或扩展),它实际上尚未与对象关联,但当我们对其进行更改时,它将成为 User
对象状态的一部分。
该集合是特定于 Address
类的,该类是唯一可以持久保存到其中的 Python 对象类型。使用 list.append()
方法,我们可以添加一个 Address
对象
>>> a1 = Address(email_address="[email protected]")
>>> u1.addresses.append(a1)
此时,正如预期的那样,u1.addresses
集合包含新的 Address
对象
>>> u1.addresses
[Address(id=None, email_address='[email protected]')]
当我们将 Address
对象与 u1
实例的 User.addresses
集合关联时,还会发生另一个行为,即 User.addresses
关系会与 Address.user
关系同步,这样我们不仅可以从 User
对象导航到 Address
对象,还可以从 Address
对象导航回“父”User
对象
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')
这种同步是由于我们在两个 relationship()
对象之间使用 relationship.back_populates
参数引起的。此参数指定另一个 relationship()
,应该对其进行互补的属性赋值/列表修改。它在另一个方向上也同样有效,即如果我们创建一个新的 Address
对象并将其分配给它的 Address.user
属性,该 Address
将成为该 User
对象的 User.addresses
集合的一部分
>>> a2 = Address(email_address="[email protected]", user=u1)
>>> u1.addresses
[Address(id=None, email_address='[email protected]'), Address(id=None, email_address='[email protected]')]
实际上,我们在Address
构造函数中使用了user
参数作为关键字参数,它与在Address
类上声明的任何其他映射属性一样被接受。它等同于在事实之后分配Address.user
属性。
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
将对象级联到会话中¶
我们现在有了一个User
和两个Address
对象,它们在内存中以双向结构相关联,但正如之前在使用 ORM 工作单元模式插入行中所述,这些对象被称为处于瞬态状态,直到它们与Session
对象关联。
我们使用仍在进行的Session
,并注意到当我们将Session.add()
方法应用于领先的User
对象时,相关的Address
对象也会被添加到同一个Session
中。
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True
上述行为,即Session
接收到一个User
对象,并沿着User.addresses
关系定位相关联的Address
对象,被称为保存-更新级联,将在 ORM 参考文档的级联中详细讨论。
现在这三个对象处于待处理状态;这意味着它们已准备好成为 INSERT 操作的主题,但尚未执行;所有三个对象都还没有分配主键,此外,a1
和a2
对象有一个名为user_id
的属性,它引用了具有指向user_account.id
列的ForeignKeyConstraint
的Column
;这些也是None
,因为这些对象还没有与实际的数据库行关联。
>>> print(u1.id)
None
>>> print(a1.user_id)
None
在这个阶段,我们可以看到工作单元过程提供的极大的实用性;回想一下在INSERT 通常会自动生成“values”子句部分中,使用一些复杂的语法将行插入到user_account
和address
表中,以便自动将address.user_id
列与user_account
行的那些列关联。此外,必须先发出user_account
行的 INSERT,然后再发出address
行的 INSERT,因为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)] ('[email protected]', 6)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('[email protected]', 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
,我们也可以访问它。由于此集合包含来自address
表的其他行集,因此当我们也访问此集合时,我们再次看到一个延迟加载发出以检索对象。
>>> 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='[email protected]'), Address(id=5, email_address='[email protected]')]
SQLAlchemy ORM 中的集合和相关属性在内存中是持久的;一旦集合或属性被填充,就不会再发出 SQL,直到该集合或属性失效。我们可以再次访问u1.addresses
,并添加或删除项目,这不会产生任何新的 SQL 调用。
>>> u1.addresses
[Address(id=4, email_address='[email protected]'), Address(id=5, email_address='[email protected]')]
虽然延迟加载发出的加载如果我们不采取明确的步骤来优化它,可能会很快变得昂贵,但至少延迟加载的网络已经过相当好的优化,不会执行冗余的工作;由于u1.addresses
集合已刷新,根据身份映射,这些实际上与我们一直在处理的a1
和a2
对象是相同的Address
实例,因此我们已经完成了加载此特定对象图中的所有属性。
>>> a1
Address(id=4, email_address='[email protected]')
>>> a2
Address(id=5, email_address='[email protected]')
关系如何加载或不加载是一个单独的话题。在本节后面的加载器策略中将对这些概念做一些额外的介绍。
在查询中使用关系¶
上一节介绍了relationship()
结构在处理映射类的实例时的行为,在上面,u1
、a1
和a2
是User
和Address
类的实例。在本节中,我们将介绍relationship()
的行为,因为它适用于映射类的类级别行为,它以多种方式帮助自动构建 SQL 查询。
使用关系进行联接¶
在显式 FROM 子句和联接和设置 ON 子句部分中,介绍了使用Select.join()
和Select.join_from()
方法来组合 SQL JOIN 子句。为了描述如何联接表,这些方法要么推断基于表元数据结构中存在单个明确的ForeignKeyConstraint
对象来联接两个表,要么我们可以提供一个明确的 SQL 表达式结构来指示特定的 ON 子句。
在使用 ORM 实体时,可以使用另一种机制来帮助我们设置联接的 ON 子句,即使用我们在用户映射中设置的relationship()
对象,如声明映射类中所示。与relationship()
对应的类绑定属性可以作为Select.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
映射上存在 ORM relationship()
不会被 Select.join()
或 Select.join_from()
用来推断 ON 子句,除非我们显式指定它。这意味着,如果我们从 User
连接到 Address
且没有 ON 子句,它之所以能工作,是因为两个映射的 Table
对象之间存在 ForeignKeyConstraint
,而不是因为 User
和 Address
类上的 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 查询指南 中,其中包含更多使用 Select.join()
和 Select.join_from()
与 relationship()
构造的示例。
关系 WHERE 操作符¶
与 relationship()
一起提供的一些其他 SQL 生成助手通常在构建语句的 WHERE 子句时非常有用。请参阅 关系 WHERE 操作符 部分,该部分位于 ORM 查询指南 中。
另请参阅
关系 WHERE 操作符 部分,该部分位于 ORM 查询指南 中。
加载策略¶
在 加载关系 部分中,我们介绍了这样一个概念:当我们使用映射的对象实例时,访问使用 relationship()
映射的属性时,默认情况下,当集合没有填充时,将发出一个 延迟加载 来加载此集合中应该存在的对象。
延迟加载是最著名的 ORM 模式之一,也是最具争议的模式之一。当内存中数十个 ORM 对象都引用了一些未加载的属性时,对这些对象的常规操作会引发许多额外的查询,这些查询会加起来(也称为 N 加 1 问题),更糟糕的是,这些查询是隐式发出的。这些隐式查询可能不会被注意到,也可能在尝试访问数据库时没有可用的数据库事务而导致错误,或者当使用其他并发模式(如 asyncio)时,实际上根本无法正常工作。
与此同时,当延迟加载与所使用的并发方法兼容且不会造成其他问题时,它是一种非常流行且实用的模式。出于这些原因,SQLAlchemy 的 ORM 非常重视能够控制和优化这种加载行为。
最重要的是,有效使用 ORM 延迟加载的第一步是**测试应用程序、打开 SQL 回显并观察发出的 SQL**。如果出现许多看起来非常类似于可以更高效地合并到一个查询中的冗余 SELECT 语句,或者在已与 Session
断开连接的对象上发生了不合适的加载,这时就需要考虑使用**加载策略**。
加载策略由对象表示,可以使用 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()
的默认值进行配置,使用 relationship.lazy
选项,例如:
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"
)
每个加载策略对象都会向语句中添加一些信息,这些信息稍后会被 Session
使用,当 Session
决定如何加载各种属性以及/或在访问属性时如何表现时,这些信息会被使用。
下面的部分将介绍一些最常用的加载策略。
选择加载¶
在现代 SQLAlchemy 中,最实用的加载器是 selectinload()
加载器选项。此选项解决了“N 加 1”问题中最常见的一种形式,即一组引用相关集合的对象。 selectinload()
将确保使用单个查询预加载一系列对象的所有特定集合。它使用一种 SELECT 形式,在大多数情况下可以仅针对相关表发出此 SELECT 形式,而不会引入 JOIN 或子查询,并且只查询那些集合尚未加载的父对象。下面,我们通过加载所有 User
对象及其所有相关 Address
对象来说明 selectinload()
;虽然我们只调用了 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 ([email protected])
sandy ([email protected], [email protected])
patrick ()
squidward ()
ehkrabs ()
pkrabs ([email protected], [email protected])
另请参阅
SELECT IN 加载 - 位于 关系加载技术 中
加入加载¶
joinedload()
积极加载策略是 SQLAlchemy 中最古老的积极加载器,它使用 JOIN(可能是外连接或内连接,具体取决于选项)来增强传递到数据库的 SELECT 语句,从而加载相关对象。
joinedload()
策略最适合加载相关的多对一对象,因为这只需要在任何情况下都会被获取的主实体行中添加额外的列。为了提高效率,它还接受一个选项 joinedload.innerjoin
,以便在以下情况下可以使用内连接而不是外连接,我们知道所有 Address
对象都与一个关联的 User
对象关联。
>>> 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
[...] ()
[email protected] spongebob
[email protected] sandy
[email protected] sandy
[email protected] pkrabs
[email protected] pkrabs
joinedload()
也适用于集合,即一对多关系,但是它会以递归方式将主行按相关项数进行倍增,从而在嵌套集合和/或更大的集合的情况下,将结果集发送的数据量增加几个数量级,因此,在每种情况下,使用 joinedload()
还是其他选项(如 selectinload()
)应该根据具体情况进行评估。
需要注意的是,包含的 Select
语句的 WHERE 和 ORDER BY 条件 **不会针对 joinedload() 呈现的表**。在上面,可以在 SQL 中看到,对 user_account
表应用了 **匿名别名**,因此无法在查询中直接寻址。此概念将在 加入式预加载的禅意 部分详细讨论。
提示
需要注意的是,多对一预加载通常没有必要,因为在常见情况下,“N+1” 问题并不常见。当许多对象都引用同一个相关对象时,例如许多 Address
对象都引用同一个 User
,SQL 将仅针对该 User
对象发出一次,使用正常的延迟加载。延迟加载例程将在当前 Session
中通过主键查找相关对象,在可能的情况下不会发出任何 SQL。
显式联接 + 预加载¶
如果我们要加载 Address
行,同时使用 Select.join()
等方法连接到 user_account
表来渲染 JOIN,我们也可以利用该 JOIN 来预加载每个返回的 Address
对象上的 Address.user
属性的内容。这实际上是我们使用“加入式预加载”,但自己渲染 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',)
[email protected] pkrabs
[email protected] 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
Raiseload¶
另一个值得一提的加载器策略是 raiseload()
。此选项用于完全阻止应用程序出现 N+1 问题,通过使通常的延迟加载改为抛出错误。它有两个变体,通过 raiseload.sql_only
选项控制,要么阻止需要 SQL 的延迟加载,要么阻止所有“加载”操作,包括那些只需要查询当前 Session
的操作。
使用 raiseload()
的一种方法是在 relationship()
本身配置它,通过将 relationship.lazy
设置为 "raise_on_sql"
,这样对于特定映射,特定关系将永远不会尝试发出 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")
使用这种映射,应用程序被阻止延迟加载,表明特定查询需要指定加载器策略。
>>> 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
对象的 Address.user
属性未加载,但该 User
对象在同一个 Session
中本地存在,那么“raiseload”策略将不会引发错误。
另请参阅
flambé! the dragon and The Alchemist image designs created and generously donated by Rotem Yaari.
使用 Sphinx 7.2.6 创建。文档上次生成时间:2024 年 11 月 8 日星期五东部时间上午 8:41:19