版本: 2.0.36 |发行日期: 2024 年 10 月 15 日


SQLAlchemy 2.0 文档


SQLAlchemy 2.0 文档


家


SQLAlchemy Unified 教程


  • 建立连接 - 引擎

  • 使用事务和 DBAPI

  • 使用数据库元数据

  • 使用数据

  • 使用 ORM 进行数据操作

  • 使用 ORM 相关对象¶

    • 保留和加载关系

      • 将对象级联到 Session 中

    • 加载关系

    • 在查询中使用关系

      • 使用关系进行联接

      • 关系 WHERE 运算符

    • 加载器策略

      • 选择加载

      • 联合载荷

      • 显式 Join + 预先加载

      • 提升负载

  • 延伸阅读


家


  • 上一篇:使用 ORM 进行数据作

  • 下一篇:延伸阅读

  • 当前位置:首页

    • SQLAlchemy Unified 教程

  • 本页内容:

    • 使用 ORM 相关对象

      • 保留和加载关系

        • 将对象级联到 Session 中

      • 加载关系

      • 在查询中使用关系

        • 使用关系进行联接

        • 关系 WHERE 运算符

      • 加载器策略

        • 选择加载

        • 联合载荷

        • 显式 Join + 预先加载

        • 提升负载


SQLAlchemy 1.4 / 2.0 教程


此页面是 SQLAlchemy Unified Tutorial 的一部分。


Previous: 使用 ORM 进行数据作 |Next: 延伸阅读


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


u1User 对象现在有一个持久化集合 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() 结构。


另请参阅


ORM Querying Guide 中的联接


关系 WHERE 运算符¶


还有一些其他种类的 SQL 生成帮助程序 relationship() 的 API 在构建 WHERE 子句。 请参阅该部分 ORM Querying Guide 中的 RELATIONSHIP WHERE 运算符。


另请参阅


ORM 查询指南中的关系 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)


另请参阅


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


联合负载¶


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


另请参阅


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


  • Joined Eager Loading 的 Zen - 详细描述了上述问题


  • 将显式连接/语句路由到预先加载的集合中 - 使用 contains_eager()


提高负载¶


另一个值得一提的 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” 策略不会引发错误。


另请参阅


使用 raiseload 防止不需要的惰性负载 - 在 Relationship Loading Techniques 中


SQLAlchemy 1.4 / 2.0 教程


下一个教程部分:延伸阅读


Previous: 使用 ORM 进行数据作Next: 延伸阅读

© 版权所有 2007-2025,SQLAlchemy 作者和贡献者。


燃烧!龙和炼金术士形象设计由 Rotem Yaari 创作并慷慨捐赠。


使用 Sphinx 7.2.6 创建。 文档上一次生成时间:美国东部标准时间 2025 年 1 月 7 日星期二下午 01:43:01