SqlAlchemy 2.0 中文文档(十九)(2)

本文涉及的产品
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
云数据库 RDS SQL Server,基础系列 2核4GB
简介: SqlAlchemy 2.0 中文文档(十九)

SqlAlchemy 2.0 中文文档(十九)(1)https://developer.aliyun.com/article/1562930


连接急切加载的禅意

由于连接的急切加载似乎与使用 Select.join() 的方式有很多相似之处,因此人们经常困惑于何时以及如何使用它。至关重要的是要理解这样的区别,即虽然 Select.join() 用于更改查询的结果,但 joinedload() 费尽心思地更改查询的结果,而是隐藏所渲染的连接的效果,仅允许相关对象存在。

加载器策略背后的理念是,任何一组加载方案都可以应用于特定查询,结果不会改变 - 只有用于完全加载相关对象和集合所需的  SQL  语句数量会改变。一个特定的查询可能首先使用所有惰性加载。在上下文中使用后,可能会发现总是访问特定属性或集合,并且更改这些属性的加载策略更有效。该策略可以更改而不必对查询进行其他修改,结果将保持不变,但会发出更少的  SQL 语句。理论上(实际上基本如此),对 Select 所做的任何操作都不会使其根据加载器策略的更改而加载不同的主对象或相关对象集。

特别地,joinedload()是如何实现不以任何方式影响返回的实体行的结果的,这是因为它为添加到查询中的连接创建了一个匿名别名,因此它们不能被查询的其他部分引用。例如,下面的查询使用joinedload()来创建从usersaddresses的 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()的用法只涉及加载结果中每个UserUser.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 的作用非常不同。一个将完全匹配一个行,即UserAddress的连接,其中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 子句中。因为 UserAddress 之间的关系有一个简单的主键连接条件,并且提供了 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()选项之前还是之后,但如果传递了每个包含"*"的多个选项,则最后一个将生效。

按实体的通配符加载策略

通配符加载策略的变体是能够根据每个实体设置策略的能力。例如,如果查询UserAddress,我们可以指示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()等方法。下面,我们指定了UserAddress之间的连接,并将其另外建立为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

相关实践学习
使用SQL语句管理索引
本次实验主要介绍如何在RDS-SQLServer数据库中,使用SQL语句管理索引。
SQL Server on Linux入门教程
SQL Server数据库一直只提供Windows下的版本。2016年微软宣布推出可运行在Linux系统下的SQL Server数据库,该版本目前还是早期预览版本。本课程主要介绍SQLServer On Linux的基本知识。 相关的阿里云产品:云数据库RDS&nbsp;SQL Server版 RDS SQL Server不仅拥有高可用架构和任意时间点的数据恢复功能,强力支撑各种企业应用,同时也包含了微软的License费用,减少额外支出。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/sqlserver
相关文章
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(十七)(2)
SqlAlchemy 2.0 中文文档(十七)
46 4
|
5月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(十七)(3)
SqlAlchemy 2.0 中文文档(十七)
43 4
|
5月前
|
SQL 关系型数据库 API
SqlAlchemy 2.0 中文文档(十七)(4)
SqlAlchemy 2.0 中文文档(十七)
94 4
|
5月前
|
SQL Java Go
SqlAlchemy 2.0 中文文档(十九)(1)
SqlAlchemy 2.0 中文文档(十九)
46 1
|
5月前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(二十二)(3)
SqlAlchemy 2.0 中文文档(二十二)
29 5
|
5月前
|
SQL 测试技术 Go
SqlAlchemy 2.0 中文文档(十八)(3)
SqlAlchemy 2.0 中文文档(十八)
31 1
|
5月前
|
SQL 测试技术 Go
SqlAlchemy 2.0 中文文档(十八)(5)
SqlAlchemy 2.0 中文文档(十八)
31 1
|
5月前
|
SQL 存储 大数据
SqlAlchemy 2.0 中文文档(十八)(1)
SqlAlchemy 2.0 中文文档(十八)
44 1
|
5月前
|
SQL 前端开发 Go
SqlAlchemy 2.0 中文文档(十八)(4)
SqlAlchemy 2.0 中文文档(十八)
32 1
|
5月前
|
SQL 测试技术 Go
SqlAlchemy 2.0 中文文档(十八)(2)
SqlAlchemy 2.0 中文文档(十八)
31 1