SqlAlchemy 2.0 中文文档(十九)(1)https://developer.aliyun.com/article/1562930
连接急切加载的禅意
由于连接的急切加载似乎与使用 Select.join()
的方式有很多相似之处,因此人们经常困惑于何时以及如何使用它。至关重要的是要理解这样的区别,即虽然 Select.join()
用于更改查询的结果,但 joinedload()
费尽心思地不更改查询的结果,而是隐藏所渲染的连接的效果,仅允许相关对象存在。
加载器策略背后的理念是,任何一组加载方案都可以应用于特定查询,结果不会改变 - 只有用于完全加载相关对象和集合所需的 SQL 语句数量会改变。一个特定的查询可能首先使用所有惰性加载。在上下文中使用后,可能会发现总是访问特定属性或集合,并且更改这些属性的加载策略更有效。该策略可以更改而不必对查询进行其他修改,结果将保持不变,但会发出更少的 SQL 语句。理论上(实际上基本如此),对 Select
所做的任何操作都不会使其根据加载器策略的更改而加载不同的主对象或相关对象集。
特别地,joinedload()
是如何实现不以任何方式影响返回的实体行的结果的,这是因为它为添加到查询中的连接创建了一个匿名别名,因此它们不能被查询的其他部分引用。例如,下面的查询使用joinedload()
来创建从users
到addresses
的 LEFT OUTER JOIN,但是针对Address.email_address
添加的ORDER BY
是无效的 - 查询中未命名实体Address
:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import joinedload >>> stmt = ( ... select(User) ... .options(joinedload(User.addresses)) ... .filter(User.name == "spongebob") ... .order_by(Address.email_address) ... ) >>> result = session.scalars(stmt).unique().all() SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ORDER BY addresses.email_address <-- this part is wrong ! ['spongebob']
上面,ORDER BY addresses.email_address
是无效的,因为addresses
不在 FROM 列表中。加载User
记录并按电子邮件地址排序的正确方法是使用Select.join()
:
>>> from sqlalchemy import select >>> stmt = ( ... select(User) ... .join(User.addresses) ... .filter(User.name == "spongebob") ... .order_by(Address.email_address) ... ) >>> result = session.scalars(stmt).unique().all() SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id WHERE users.name = ? ORDER BY addresses.email_address ['spongebob']
上述语句当然与先前的语句不同,因为从addresses
中提取的列根本没有包含在结果中。我们可以重新添加joinedload()
,这样就有两个连接 - 一个是我们正在排序的连接,另一个是匿名使用的连接,用于加载User.addresses
集合的内容:
>>> stmt = ( ... select(User) ... .join(User.addresses) ... .options(joinedload(User.addresses)) ... .filter(User.name == "spongebob") ... .order_by(Address.email_address) ... ) >>> result = session.scalars(stmt).unique().all() SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ORDER BY addresses.email_address ['spongebob']
我们上面看到的是我们对Select.join()
的用法是提供我们希望在随后的查询条件中使用的 JOIN 子句,而我们对joinedload()
的用法只涉及加载结果中每个User
的User.addresses
集合。在这种情况下,这两个连接很可能看起来是多余的 - 它们就是。如果我们只想使用一个 JOIN 来加载集合并进行排序,我们可以使用contains_eager()
选项,下面将介绍如何将显式的 JOIN/语句路由到急加载的集合中。但要了解joinedload()
为什么会产生这样的结果,考虑一下如果我们在特定的Address
上进行过滤:
>>> stmt = ( ... select(User) ... .join(User.addresses) ... .options(joinedload(User.addresses)) ... .filter(User.name == "spongebob") ... .filter(Address.email_address == "someaddress@foo.com") ... ) >>> result = session.scalars(stmt).unique().all() SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? AND addresses.email_address = ? ['spongebob', 'someaddress@foo.com']
上面,我们可以看到这两个 JOIN 的作用非常不同。一个将完全匹配一个行,即User
和Address
的连接,其中Address.email_address=='someaddress@foo.com'
。另一个 LEFT OUTER JOIN 将匹配与User
相关的所有Address
行,并且仅用于为返回的那些User
对象填充User.addresses
集合。
通过改变 joinedload()
的使用方式到另一种加载样式,我们可以完全独立于用于检索实际想要的 User
行的 SQL 改变集合的加载方式。以下我们将 joinedload()
改变为 selectinload()
:
>>> stmt = ( ... select(User) ... .join(User.addresses) ... .options(selectinload(User.addresses)) ... .filter(User.name == "spongebob") ... .filter(Address.email_address == "someaddress@foo.com") ... ) >>> result = session.scalars(stmt).all() SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id WHERE users.name = ? AND addresses.email_address = ? ['spongebob', 'someaddress@foo.com'] # ... selectinload() emits a SELECT in order # to load all address records ...
当使用联接式的急加载时,如果查询包含影响联接外返回的行的修改器,比如使用 DISTINCT、LIMIT、OFFSET 或等效的修改器时,完成的语句首先被包裹在一个子查询中,并且专门用于联接式的急加载的联接应用于子查询。SQLAlchemy 的联接式急加载会走出额外的一步,然后再走出额外的十步,绝对确保它不会影响查询的最终结果,只会影响集合和相关对象的加载方式,无论查询的格式如何。
另请参阅
将显式联接/语句路由到急加载集合 - 使用 contains_eager()
## 选择 IN 加载
在大多数情况下,选择 IN 加载是急加载对象集合的最简单和最有效的方式。唯一一个不可行的情况是当模型使用复合主键,并且后端数据库不支持具有 IN 的元组时,这目前包括 SQL Server。
使用 "selectin"
参数提供了“选择 IN”急加载,或者通过使用 selectinload()
加载器选项。这种加载样式发出一个 SELECT,该 SELECT 引用父对象的主键值,或者在一对多关系的情况下引用子对象的主键值,以便在 IN 子句中加载相关联的关系:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import selectinload >>> stmt = ( ... select(User) ... .options(selectinload(User.addresses)) ... .filter(or_(User.name == "spongebob", User.name == "ed")) ... ) >>> result = session.scalars(stmt).all() SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.name = ? OR users.name = ? ('spongebob', 'ed') SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE addresses.user_id IN (?, ?) (5, 7)
上面,第二个 SELECT 是指 addresses.user_id IN (5, 7)
,其中的 “5” 和 “7” 是之前加载的前两个 User
对象的主键值;在一批对象完全加载后,它们的主键值被注入到第二个 SELECT 的 IN
子句中。因为 User
和 Address
之间的关系有一个简单的主键连接条件,并且提供了 User
的主键值可以从 Address.user_id
派生出来,所以语句根本没有联接或子查询。
对于简单的一对多加载,也不需要使用 JOIN,因为会使用父对象的外键值:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import selectinload >>> stmt = select(Address).options(selectinload(Address.user)) >>> result = session.scalars(stmt).all() SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.id IN (?, ?) (1, 2)
提示
通过“简单”我们指的是 relationship.primaryjoin
条件表达了“一”侧的主键与“多”侧的直接外键之间的相等比较,没有任何额外的条件。
Select IN 加载还支持多对多关系,其中它当前将跨越所有三个表进行 JOIN,以将一侧的行与另一侧的行匹配。
关于这种加载方式需要知道的事情包括:
- 该策略每次会发出一个 SELECT 查询,最多查询 500 个父主键值,因为这些主键值会被渲染成 SQL 语句中的一个大型 IN 表达式。一些数据库,比如 Oracle,在 IN 表达式的大小上有一个硬限制,总体上 SQL 字符串的大小不应该是任意大的。
- 由于“selectin”加载依赖于 IN,在具有复合主键的映射中,它必须使用“元组”形式的 IN,看起来像
WHERE (table.column_a, table.column_b) IN ((?, ?), (?, ?), (?, ?))
。这种语法目前不受 SQL Server 支持,对于 SQLite 需要至少版本 3.15。SQLAlchemy 中没有特殊逻辑来提前检查哪些平台支持这种语法,如果运行在不支持的平台上,数据库将立即返回错误。SQLAlchemy 只需运行 SQL 语句以使其失败的一个优点是,如果某个特定数据库开始支持这种语法,它将无需对 SQLAlchemy 进行任何更改即可工作(就像 SQLite 的情况一样)。## 子查询急加载
传统特性
subqueryload()
预加载器在这一点上主要是传统的,已被 selectinload()
策略取代,后者设计更简单,更灵活,具有诸如 Yield Per 等功能,并在大多数情况下发出更有效的 SQL 语句。由于 subqueryload()
依赖于重新解释原始的 SELECT 语句,当给定非常复杂的源查询时,可能无法有效地工作。
对于使用复合主键的对象的急加载集合的特定情况,subqueryload()
可能仍然有用,因为 Microsoft SQL Server 后端仍然不支持“元组 IN”语法。
子查询加载在操作上类似于 selectin 急加载,但发出的 SELECT 语句是从原始语句派生的,并且具有更复杂的查询结构,类似于 selectin 急加载。
通过在 relationship.lazy
中使用 "subquery"
参数或使用 subqueryload()
加载器选项提供子查询急加载。
子查询急加载的操作是为要加载的每个关系发出第二个 SELECT 语句,跨所有结果对象一次性加载。此 SELECT 语句引用原始 SELECT 语句,该语句包装在子查询中,以便我们检索要返回的主对象的相同主键列表,然后将其与要一次性加载的所有集合成员的总和链接起来:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import subqueryload >>> stmt = select(User).options(subqueryload(User.addresses)).filter_by(name="spongebob") >>> results = session.scalars(stmt).all() SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.name = ? ('spongebob',) SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id FROM ( SELECT users.id AS users_id FROM users WHERE users.name = ?) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id, addresses.id ('spongebob',)
关于这种加载方式需要知道的事项包括:
- “子查询”加载策略发出的 SELECT 语句(与“selectin”的不同之处在于)需要一个子查询,并将继承原始查询中存在的任何性能限制。子查询本身也可能因为所使用的数据库的具体情况而产生性能惩罚。
- “子查询”加载在正确工作时会施加一些特殊的排序要求。使用
subqueryload()
与诸如Select.limit()
或Select.offset()
这样的限定修饰符的查询应该始终包含针对唯一列(如主键)的Select.order_by()
,以便由subqueryload()
发出的附加查询包含与父查询使用的相同排序。否则,内部查询可能会返回错误的行:
# incorrect, no ORDER BY stmt = select(User).options(subqueryload(User.addresses).limit(1)) # incorrect if User.name is not unique stmt = select(User).options(subqueryload(User.addresses)).order_by(User.name).limit(1) # correct stmt = ( select(User) .options(subqueryload(User.addresses)) .order_by(User.name, User.id) .limit(1) )
- 另请参阅
为什么建议在 LIMIT 中使用 ORDER BY(特别是在 subqueryload() 中)? - 详细示例 - 当在许多级别的深层急加载上使用时,“子查询”加载还会带来额外的性能/复杂性问题,因为子查询将被重复嵌套。
- “子查询”加载与由 Yield Per 提供的“批量”加载(对集合和标量关系均适用)不兼容。
出于上述原因,“selectin”策略应优先于“子查询”。
另请参阅
选择 IN 加载 ## 何种加载方式?
使用哪种加载方式通常涉及到优化 SQL 执行次数、所发出 SQL 的复杂度以及获取的数据量之间的权衡。
一对多 / 多对多集合 - selectinload()
通常是最佳的加载策略。它会发出额外的 SELECT 查询,尽可能使用少量的表,不影响原始语句,并且对于任何类型的原始查询都非常灵活。它唯一的主要限制是在使用不支持“tuple IN”的后端的复合主键表时,目前包括 SQL Server 和非常旧的 SQLite 版本;所有其他包含的后端都支持它。
多对一 - joinedload()
策略是最通用的策略。在特殊情况下,如果潜在相关值数量很少,也可以使用immediateload()
策略,因为如果相关对象已经存在,则该策略将从本地Session
获取对象,而不发出任何 SQL。
多态急加载
支持按急加载基础上的每个急加载选项进行多态选项的指定。参见 Eager Loading of Polymorphic Subtypes 部分中与with_polymorphic()
函数配合使用的PropComparator.of_type()
方法的示例。
通配符加载策略
joinedload()
、subqueryload()
、lazyload()
、selectinload()
、noload()
和raiseload()
中的每一个都可以用于为特定查询设置relationship()
加载的默认样式,影响除了在语句中另有规定的所有 relationship()
-映射属性。通过将字符串 '*'
作为这些选项中的任何一个的参数传递,可以使用此功能:
from sqlalchemy import select from sqlalchemy.orm import lazyload stmt = select(MyClass).options(lazyload("*"))
在上面的例子中,lazyload('*')
选项将取代该查询中所有正在使用的所有 relationship()
构造的 lazy
设置,但不包括那些使用 lazy='write_only'
或 lazy='dynamic'
的构造。
如果某些关系指定了lazy='joined'
或lazy='selectin'
,例如,使用lazyload('*')
将单方面地导致所有这些关系使用'select'
加载,例如,当访问每个属性时发出 SELECT 语句。
该选项不会取代查询中声明的加载选项,例如joinedload()
,selectinload()
等。下面的查询仍将使用widget
关系的 joined 加载:
from sqlalchemy import select from sqlalchemy.orm import lazyload from sqlalchemy.orm import joinedload stmt = select(MyClass).options(lazyload("*"), joinedload(MyClass.widget))
尽管joinedload()
的指令会发生,无论它出现在lazyload()
选项之前还是之后,但如果传递了每个包含"*"
的多个选项,则最后一个将生效。
按实体的通配符加载策略
通配符加载策略的变体是能够根据每个实体设置策略的能力。例如,如果查询User
和Address
,我们可以指示Address
上的所有关系使用延迟加载,同时通过首先应用Load
对象,然后指定*
作为链接选项,保持对User
的加载策略不受影响:
from sqlalchemy import select from sqlalchemy.orm import Load stmt = select(User, Address).options(Load(Address).lazyload("*"))
上面,Address
上的所有关系都将设置为延迟加载。 ## 将显式连接/语句路由到急加载集合
joinedload()
的行为是自动创建连接,使用匿名别名作为目标,其结果被路由到加载对象上的集合和标量引用中。通常情况下,查询已经包括表示特定集合或标量引用的必要连接,并且 joinedload 功能添加的连接是多余的 - 但您仍希望填充集合/引用。
对此,SQLAlchemy 提供了contains_eager()
选项。此选项的使用方式与joinedload()
选项相同,不同之处在于假定Select
对象将明确包含适当的连接,通常使用Select.join()
等方法。下面,我们指定了User
和Address
之间的连接,并将其另外建立为User.addresses
的急加载的基础:
from sqlalchemy.orm import contains_eager stmt = select(User).join(User.addresses).options(contains_eager(User.addresses))
如果语句的“eager”部分是“aliased”,则应使用PropComparator.of_type()
指定路径,这允许传递特定的aliased()
构造:
# use an alias of the Address entity adalias = aliased(Address) # construct a statement which expects the "addresses" results stmt = ( select(User) .outerjoin(User.addresses.of_type(adalias)) .options(contains_eager(User.addresses.of_type(adalias))) ) # get results normally r = session.scalars(stmt).unique().all() SELECT users.user_id AS users_user_id, users.user_name AS users_user_name, adalias.address_id AS adalias_address_id, adalias.user_id AS adalias_user_id, adalias.email_address AS adalias_email_address, (...other columns...) FROM users LEFT OUTER JOIN email_addresses AS email_addresses_1 ON users.user_id = email_addresses_1.user_id
作为参数给出的路径contains_eager()
需要是从起始实体开始的完整路径。例如,如果我们正在加载 Users->orders->Order->items->Item
,则选项将如下使用:
stmt = select(User).options(contains_eager(User.orders).contains_eager(Order.items))
使用 contains_eager() 加载自定义过滤的集合结果
当我们使用contains_eager()
时,我们正在构造用于填充集合的 SQL。由此自然地可以选择修改要存储在集合中的值,通过编写 SQL 来加载集合或标量属性的子集。
提示
SQLAlchemy 现在有一种更简单的方法来做到这一点,它允许将 WHERE 条件直接添加到加载器选项中,例如joinedload()
和 selectinload()
,使用PropComparator.and_()
。参见添加条件到加载器选项部分的示例。
此处描述的技术仍然适用于使用 SQL 条件或修饰符查询相关集合,而不仅仅是简单的 WHERE 子句。
例如,我们可以加载一个 User
对象,并通过过滤连接数据来将只特定地址急切地加载到其 .addresses
集合中,使用contains_eager()
路由,还使用 Populate Existing 确保任何已加载的集合都被覆盖:
stmt = ( select(User) .join(User.addresses) .filter(Address.email_address.like("%@aol.com")) .options(contains_eager(User.addresses)) .execution_options(populate_existing=True) )
上述查询将仅加载包含至少一个在其 email
字段中包含子字符串'aol.com'
的 Address
对象的 User
对象;User.addresses
集合将仅包含这些 Address
条目,并且不包含实际与集合关联的任何其他 Address
条目。
提示
在所有情况下,SQLAlchemy ORM 不会覆盖已加载的属性和集合,除非有指示要这样做。由于正在使用一个身份映射,通常情况下,ORM 查询返回的对象实际上已经存在并加载到内存中。因此,当使用contains_eager()
以另一种方式填充集合时,通常最好像上面示例中所示那样使用填充现有,以便已加载的集合使用新数据进行刷新。populate_existing
选项将重置已经存在的所有属性,包括待处理的更改,因此在使用它之前确保所有数据都已刷新。使用带有其默认行为的Session
,默认行为为自动刷新,已足够。
注意
我们使用contains_eager()
加载的定制集合不是“粘性”的;也就是说,下次加载此集合时,它将使用其通常的默认内容加载。如果对象过期,则该集合可能会重新加载,这在默认会话设置下发生,即每当使用 Session.commit()
、Session.rollback()
方法时,或者使用 Session.expire_all()
或 Session.expire()
方法。
参见
将条件添加到加载器选项 - 现代 API 允许在任何关系加载器选项中直接添加 WHERE 条件
SqlAlchemy 2.0 中文文档(十九)(3)https://developer.aliyun.com/article/1562932