SqlAlchemy 2.0 中文文档(三)(4)

简介: SqlAlchemy 2.0 中文文档(三)

SqlAlchemy 2.0 中文文档(三)(3)https://developer.aliyun.com/article/1562431


处理 ORM 相关对象

原文:docs.sqlalchemy.org/en/20/tutorial/orm_related_objects.html

在本节中,我们将涵盖另一个重要的 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.userrelationship() 构造与 Mapped 构造一起指示类型行为,将用于检查与 UserAddress 类映射到的 Table 对象之间的表关系。由于代表 address 表的 Table 对象具有指向 user_account 表的 ForeignKeyConstraintrelationship() 可以明确确定从 User 类到 Address 类的 一对多 关系,沿着 User.addresses 关系;user_account 表中的一个特定行可能被 address 表中的多行引用。

所有一对多关系自然对应于另一个方向的多对一关系,在本例中由Address.user指出。如上所示在两个relationship()对象上配置的relationship.back_populates参数,建立了这两个relationship()构造应被视为彼此补充;我们将在下一节中看到这是如何运作的。

持久化和加载关系

我们可以首先说明relationship()对对象实例做了什么。如果我们创建一个新的User对象,我们可以注意到当我们访问.addresses元素时有一个 Python 列表:

>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]

此对象是 Python list的 SQLAlchemy  特定版本,具有跟踪和响应对其进行的更改的能力。即使我们从未将其分配给对象,当我们访问属性时,集合也会自动出现。这类似于在使用 ORM  工作单元模式插入行中观察到的行为,在那里我们观察到,我们没有明确为其分配值的基于列的属性也会自动显示为None,而不是像 Python 通常行为一样引发AttributeError

由于u1对象仍然是瞬态,我们从u1.addresses获取的list尚未发生变异(即未被追加或扩展),因此它实际上还没有与对象关联,但随着我们对其进行更改,它将成为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() 对象之间使用的 relationship.back_populates 参数导致的。此参数命名了另一个应该发生补充属性赋值/列表变异的 relationship() 。在另一个方向上同样有效,即如果我们创建另一个 Address 对象并将其分配给其 Address.user 属性,那么该 Address 将成为该 User 对象上的 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 构造函数中使用了 user 参数作为关键字参数,它像在 Address 类上声明的任何其他映射属性一样被接受。这相当于在事后分配了 Address.user 属性:

# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1

将对象级联到会话中

我们现在有一个 User 和两个 Address 对象,它们在内存中以双向结构关联,但正如之前在 使用 ORM 单元工作模式插入行 中所指出的,这些对象被认为处于 瞬时态 ,直到它们与一个 Session 对象关联。

我们继续使用正在进行中的 Session ,注意当我们对主要的 User 对象应用 Session.add() 方法时,相关的 Address 对象也被添加到同一个 Session 中:

>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,Session 接收了一个 User 对象,并沿着 User.addresses 关系找到了相关的 Address 对象,被称为 保存-更新级联,在 ORM 参考文档的 级联 中详细讨论。

这三个对象现在处于 pending 状态;这意味着它们准备好被用于 INSERT 操作,但这还没有进行;所有三个对象都还没有分配主键,并且a1a2对象还有一个名为user_id的属性,它指向具有引用user_account.id列的Column,这些也都是None,因为这些对象还没有与真实的数据库行关联:

>>> print(u1.id)
None
>>> print(a1.user_id)
None

正是在这个阶段,我们可以看到工作单元过程提供的非常大的实用性;回想在 INSERT 通常会自动生成“values”子句一节中,使用一些复杂的语法将行插入到user_accountaddress表中,以便自动将address.user_id列与user_account行的列关联起来。此外,我们需要先为user_account行发出 INSERT,然后再为address行发出 INSERT,因为address行依赖于其父行user_accountuser_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`对象的新生成的主键时:
```py
>>> 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='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 集合被刷新,根据身份映射,这些实际上是我们已经处理过的a1a2对象中的相同的Address实例,因此我们已经完成了加载这个特定对象图中的所有属性:

>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')

关系如何加载或不加载的问题是一个独立的主题。稍后在本节的加载策略中对这些概念进行了一些补充介绍。 ## 在查询中使用关系

前一节介绍了当使用映射类的实例relationship()构造的行为,上文介绍了UserAddress类的u1a1a2实例。在本节中,我们介绍了当应用于映射类的类级行为时,relationship()的行为,它在多个方面帮助自动构建 SQL 查询。

使用关系进行连接

显式 FROM 子句和 JOINs 和设置 ON 子句章节介绍了使用Select.join()Select.join_from()方法来组合 SQL JOIN 子句。为了描述如何在表之间进行连接,这些方法要么根据表元数据结构中存在的单个明确的ForeignKeyConstraint对象推断出 ON 子句,该对象链接了这两个表,要么我们可以提供一个明确的 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() 的存在,如果我们没有指定 ON 子句,将不会被 Select.join()Select.join_from() 用于推断 ON 子句。这意味着,如果我们从 User 连接到 Address 而没有 ON 子句,它会工作是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是 UserAddress 类上的 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() 构造的更多示例。

请参见

连接 在 ORM 查询指南 ### 关系 WHERE 运算符

relationship() 还配备了一些额外的 SQL 生成辅助工具,当构建语句的 WHERE 子句时通常很有用。请参阅 关系 WHERE 运算符 在 ORM 查询指南 中的部分。

请参见

关系 WHERE 运算符在 ORM 查询指南中 ## 加载策略

在加载关系部分,我们介绍了这样一个概念,当我们使用映射对象的实例时,访问使用relationship()映射的属性时,在默认情况下,如果集合未填充,则会发出延迟加载以加载应该存在于此集合中的对象。

延迟加载是最著名的 ORM 模式之一,也是最具争议的模式之一。当内存中有几十个 ORM  对象分别引用少量未加载的属性时,对这些对象的常规操作可能会产生许多额外的查询,这些查询可能会累积(也称为 N  加一问题),更糟糕的是它们是隐式发出的。这些隐式查询可能不会被注意到,在数据库事务不再可用时尝试执行它们时可能会导致错误,或者在使用诸如  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在决定各种属性在访问时应如何加载和/或行为时使用。

下面的部分将介绍一些最常用的加载器策略。

参见

关系加载技术中的两个部分:

  • 在映射时配置加载器策略 - 配置在relationship()上的策略的详细信息
  • 使用加载器选项进行关系加载 - 使用查询时加载策略的详细信息

Selectin Load

在现代 SQLAlchemy 中最有用的加载器是selectinload()加载器选项。该选项解决了最常见形式的“N 加一”问题,即一组对象引用相关集合。selectinload()将确保立即使用单个查询加载整个系列对象的特定集合。它使用一种 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  (spongebob@sqlalchemy.org)
sandy  (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick  ()
squidward  ()
ehkrabs  ()
pkrabs  (pearl.krabs@gmail.com, pearl@aol.com)

参见

选择 IN 加载 - 在关系加载技术中

Joined Load

joinedload()预加载策略是 SQLAlchemy 中最古老的预加载器,它通过在传递给数据库的 SELECT 语句中添加一个 JOIN(根据选项可能是外连接或内连接)来增强,然后可以加载相关对象。

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
[...]  ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

joinedload()也适用于集合,意味着一对多关系,但它会以递归方式将每个相关项乘以主行,从而增加通过结果集发送的数据量,对于嵌套集合和/或较大集合,这会使数据量成倍增长,因此应该根据具体情况评估其与其他选项(例如selectinload())的使用。

需要注意的是,封闭Select语句的 WHERE 和 ORDER BY 条件不会针对 joinedload()生成的表。上面的例子中,可以看到 SQL 中对user_account表应用了一个匿名别名,以便在查询中无法直接寻址。这个概念在加入式预加载的禅意一节中有更详细的讨论。

提示

需要注意的是,多对一的预加载通常是不必要的,因为“N 加一”问题在常见情况下要少得多。当许多对象都引用相同的相关对象时,例如每个都引用相同User的许多Address对象时,SQL 将仅对该User对象发出一次,使用普通的惰性加载。惰性加载例程将在当前Session中尽可能地通过主键查找相关对象,而不在可能时发出任何 SQL。

另请参阅

加入式预加载 - 在关系加载技术中

明确的连接 + 预加载

如果我们在连接到 user_account 表时加载 Address 行,使用诸如 Select.join() 之类的方法来渲染 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',)
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 

另请参阅

关系加载技术中的两个部分:

  • 连接急切加载的禅意 - 详细描述了上述问题
  • 将显式连接/语句路由到急切加载的集合 - 使用 contains_eager()

Raiseload

值得一提的另一个加载器策略是 raiseload() 。此选项用于通过导致通常将是延迟加载的操作引发错误来完全阻止应用程序遇到 N 加一 问题。它有两个变体,通过 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”策略将不会引发错误。

另请参阅

使用 raiseload 阻止不必要的懒加载 - 在关系加载技术中

持久化和加载关系

我们可以先说明 relationship() 对象实例的作用。如果我们创建一个新的 User 对象,我们可以注意到当我们访问 .addresses 元素时会有一个 Python 列表:

>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]

此对象是 Python list 的 SQLAlchemy 特定版本,具有跟踪和响应对其进行的更改的能力。当我们访问属性时,集合也会自动出现,即使我们从未将其分配给对象。这类似于在 使用 ORM 工作单元模式插入行 中注意到的行为,即我们没有明确为其分配值的基于列的属性也会自动显示为 None,而不是像 Python 的通常行为那样引发 AttributeError

由于 u1 对象仍然是 瞬态,并且我们从 u1.addresses 得到的 list 尚未被改变(即未被添加或扩展),因此实际上尚未与对象关联,但是当我们对其进行更改时,它将成为 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()对象之间使用了relationship.back_populates参数。该参数命名了另一个应进行互补属性赋值/列表变异的relationship()。在另一个方向上同样有效,即如果我们创建另一个Address对象并将其分配给其Address.user属性,该Address将成为User对象上的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构造函数中使用了user参数作为关键字参数,这与在Address类上声明的任何其他映射属性一样被接受。这相当于事后对Address.user属性进行赋值:

# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1

将对象级联到会话中

现在我们有一个User和两个Address对象,在内存中以双向结构关联,但如前所述,在使用 ORM 工作单元模式插入行中,这些对象被称为处于瞬态状态,直到它们与一个Session对象关联为止。

我们利用的是仍在进行中的Session,请注意,当我们对主User对象应用Session.add()方法时,相关的Address对象也会被添加到同一个Session中:

>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,即Session接收到一个User对象,并沿着User.addresses关系定位相关的Address对象的行为,被称为保存更新级联,并在 ORM 参考文档中详细讨论,链接地址为 Cascades。

这三个对象现在处于 挂起 状态;这意味着它们已经准备好成为 INSERT 操作的对象,但这还没有进行;所有三个对象目前还没有分配主键,并且此外,a1a2 对象具有一个名为 user_id 的属性,该属性指向具有引用 user_account.id 列的 Column,这些属性也是 None,因为这些对象尚未与真实的数据库行关联:

>>> print(u1.id)
None
>>> print(a1.user_id)
None

此时,我们可以看到工作单元流程提供的非常大的实用性;回想一下在 INSERT 通常会自动生成“values”子句 中,行是如何插入到 user_accountaddress 表中的,使用一些复杂的语法来自动将 address.user_id 列与 user_account 表中的列关联起来。此外,我们必须首先为 user_account 表中的行发出 INSERT,然后是 address 表中的行,因为 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 
```### 将对象级联到会话中
现在,我们在内存中有一个双向结构的 `User` 对象和两个 `Address` 对象,但正如之前在 使用 ORM 工作单元模式插入行 中所述,这些对象被认为处于 瞬时 状态,直到它们与一个 `Session` 对象关联为止。
我们利用的是仍在进行中的 `Session`,请注意,当我们将 `Session.add()` 方法应用于主 `User` 对象时,相关的 `Address` 对象也会被添加到同一个 `Session` 中:
```py
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,其中Session接收到一个 User 对象,并沿着 User.addresses 关系跟踪以找到相关的 Address 对象,被称为save-update cascade,并且在 ORM 参考文档的 Cascades 中有详细讨论。

这三个对象现在处于 pending 状态;这意味着它们已准备好成为 INSERT 操作的主体,但还没有进行;这三个对象都还没有分配主键,并且此外,a1a2 对象具有一个名为 user_id 的属性,它指向具有引用 user_account.id 列的Column;由于这些对象尚未与真实的数据库行关联,因此这些值也都是 None

>>> print(u1.id)
None
>>> print(a1.user_id)
None

此时,我们可以看到工作单元流程提供的非常大的实用性;回想一下,在 INSERT 通常自动生成“values”子句一节中,我们使用一些复杂的语法将行插入到 user_accountaddress 表中,以便自动将 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)]  ('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,我们也可以访问它。由于此集合包含来自 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='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 集合已经刷新,根据标识映射,这些实际上是我们已经处理过的 a1a2 对象的同一 Address 实例,所以我们已经完成了加载此特定对象图中的所有属性:

>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')

关系如何加载或不加载是一个独立的主题。稍后在本节的 加载器策略 中会对这些概念进行一些额外的介绍。

在查询中使用关系

前一节介绍了当与映射类的实例一起使用时 relationship() 构造的行为,上面是 UserAddress 类的 u1a1a2 实例。在本节中,我们将介绍当应用于映射类的类级行为relationship() 的行为,在这里,它以几种方式帮助自动构建 SQL 查询。

使用关系进行连接

显式的 FROM 子句和 JOINs 和 设置 ON 子句 章节介绍了使用 Select.join()Select.join_from() 方法来组合 SQL JOIN 子句。为了描述如何在表之间进行连接,这些方法要么**根据表元数据结构中链接两个表的单个明确的 ForeignKeyConstraint 对象推断出 ON 子句,要么我们可以提供一个明确的 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 

如果我们没有指定 ON 子句,则映射上的 ORM relationship()Select.join()Select.join_from() 的存在不会用于推断 ON 子句。这意味着,如果我们从 User 连接到 Address 而没有 ON 子句,这是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是由于 UserAddress 类上的 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() 构造的更多示例。

另请参阅

ORM 查询指南中的连接 ### Relationship WHERE 运算符

还有一些额外的 SQL 生成辅助程序,随着 relationship() 一起提供,当构建语句的 WHERE 子句时通常很有用。请参阅 ORM 查询指南中的 Relationship WHERE 运算符一节。

另请参阅

ORM 查询指南中的关系 WHERE 运算符 ### 使用关系进行连接 在 ORM 查询指南

明确的 FROM 子句和 JOIN 和设置 ON 子句部分介绍了使用Select.join()Select.join_from()方法组成 SQL JOIN 子句的用法。为了描述如何在表之间进行连接,这些方法根据表元数据结构中链接两个表的单一明确ForeignKeyConstraint对象的存在推断 ON 子句,或者我们可以提供一个明确的 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 子句。这意味着,如果我们从 UserAddress 进行连接而没有 ON 子句,这是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是 UserAddress 类上的 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 查询指南中查看连接(Joins)部分,了解如何使用Select.join()Select.join_from()以及relationship()构造的更多示例。

另请参阅

ORM 查询指南中的连接(Joins)

关系 WHERE 运算符

在构建语句的 WHERE 子句时,relationship()还附带了一些其他类型的 SQL 生成助手,通常在构建过程中非常有用。请查看 ORM 查询指南中的关系 WHERE 运算符部分。

另请参阅

在 ORM 查询指南中的关系 WHERE 运算符部分


SqlAlchemy 2.0 中文文档(三)(5)https://developer.aliyun.com/article/1562433

相关文章
|
5月前
|
测试技术 API 数据库
SqlAlchemy 2.0 中文文档(九)(5)
SqlAlchemy 2.0 中文文档(九)
35 0
|
5月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(五)(5)
SqlAlchemy 2.0 中文文档(五)
64 4
|
5月前
|
存储 SQL API
SqlAlchemy 2.0 中文文档(四)(5)
SqlAlchemy 2.0 中文文档(四)
41 3
|
5月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(一)(3)
SqlAlchemy 2.0 中文文档(一)
94 1
|
5月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(十)(3)
SqlAlchemy 2.0 中文文档(十)
39 1
|
5月前
|
SQL 自然语言处理 数据库
SqlAlchemy 2.0 中文文档(二)(3)
SqlAlchemy 2.0 中文文档(二)
70 2
|
5月前
|
SQL 测试技术 Python
SqlAlchemy 2.0 中文文档(二)(1)
SqlAlchemy 2.0 中文文档(二)
63 2
|
5月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(五)(1)
SqlAlchemy 2.0 中文文档(五)
49 0
|
5月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(三)(5)
SqlAlchemy 2.0 中文文档(三)
39 0
|
5月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(十)(2)
SqlAlchemy 2.0 中文文档(十)
33 0