SqlAlchemy 2.0 中文文档(三)(4)https://developer.aliyun.com/article/1562432
加载策略
在加载关系部分,我们介绍了一个概念,即当我们处理映射对象的实例时,默认情况下访问使用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 plus one”问题的最常见形式,即一组对象引用相关集合。selectinload()
将确保通过单个查询一次性加载一系列对象的特定集合。它使用的 SELECT 形式在大多数情况下可以只针对相关表发出,而不需要引入 JOIN 或子查询,并且仅查询那些尚未加载集合的父对象。下面我们通过加载所有 User
对象及其所有相关的 Address
对象来说明 selectinload()
;虽然我们只调用一次 Session.execute()
,但在访问数据库时实际上发出了两个 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 中最古老的立即加载器,它通过将传递给数据库的 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 加一”问题在常见情况下不太普遍。当许多对象都引用同一个相关对象时,比如许多Address
对象都引用同一个User
时,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 加 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”策略就不会引发错误。
请参阅
使用 raiseload 防止不必要的惰性加载 - 在关系加载技术中
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 加载 - 在关系加载技术中
加载连接
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()
之类的方法来渲染连接,我们还可以利用该连接以便在每个返回的Address
对象上急切加载Address.user
属性的内容。这本质上是我们正在使用“联接式预加载”,但是自己渲染连接。通过使用contains_eager()
选项实现了这种常见用例。该选项与joinedload()
非常相似,只是它假设我们已经自己设置了连接,并且它仅指示应该将 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 防止不必要的惰性加载 - 在 关系加载技术 中
(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: ```py >>> 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 防止不必要的惰性加载 - 在 关系加载技术 中