关系加载技术
原文:
docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html
关于本文档
本节详细介绍了如何加载相关对象。读者应熟悉关系配置和基本用法。
大多数示例假定“用户/地址”映射设置类似于在选择设置中所示的设置。
SQLAlchemy 的一个重要部分是在查询时提供对相关对象加载方式的广泛控制。所谓“相关对象”是指在映射器上使用relationship()
配置的集合或标量关联。这种行为可以在映射器构造时使用relationship()
函数的relationship.lazy
参数进行配置,以及通过使用ORM 加载选项与Select
构造函数一起使用。
关系加载分为三类:延迟加载、急切加载和无加载。延迟加载指的是从查询返回的对象,相关对象一开始并未加载。当在特定对象上首次访问给定集合或引用时,会发出额外的 SELECT 语句,以加载请求的集合。
急切加载是指从查询返回的对象中,相关集合或标量引用已经提前加载。ORM 实现这一点要么通过增加通常会发出的 SELECT 语句以加载相关行,要么通过在主要语句之后发出额外的 SELECT 语句以一次性加载集合或标量引用。
“无”加载指的是在给定关系上禁用加载,要么属性为空且从不加载,要么在访问时引发错误,以防止不必要的延迟加载。
关系加载样式总结
关系加载的主要形式包括:
- 惰性加载 - 可通过
lazy='select'
或lazyload()
选项使用,这是一种加载形式,它在属性访问时发出 SELECT 语句,以惰性加载单个对象上的相关引用。惰性加载是所有未明示relationship.lazy
选项的relationship()
构造的默认加载方式。惰性加载详见惰性加载。 - 选择 IN 加载 - 可通过
lazy='selectin'
或selectinload()
选项使用,这种加载形式会发出第二个(或更多)SELECT 语句,将父对象的主键标识符组装到一个 IN 子句中,以便通过主键一次加载所有相关集合/标量引用。选择 IN 加载详见选择 IN 加载。 - 关联加载 - 可通过
lazy='joined'
或joinedload()
选项使用,这种加载方式会在给定的 SELECT 语句上应用 JOIN,以便相关行在同一结果集中加载。关联的及时加载详见关联的及时加载。 - 引发加载 - 可通过
lazy='raise'
、lazy='raise_on_sql'
或raiseload()
选项使用,这种加载形式在通常会发生惰性加载的时候被触发,但它会引发一个 ORM 异常,以防止应用程序进行不必要的惰性加载。引发加载的简介详见使用 raiseload 防止不想要的惰性加载。 - 子查询加载 - 可通过
lazy='subquery'
或subqueryload()
选项使用,这种加载方式会发出第二个 SELECT 语句,该语句重新陈述了原始查询嵌入到子查询中,然后将该子查询与要加载的相关表 JOIN 起来,以一次加载所有相关集合/标量引用的成员。子查询的及时加载详见子查询的及时加载。 - 仅写入加载 - 可通过
lazy='write_only'
获得,或通过在Relationship
对象的左侧使用WriteOnlyMapped
注解来标注。这种仅限集合的加载方式产生了一种替代的属性装载机制,从不隐式地从数据库加载记录,而是仅允许使用WriteOnlyCollection.add()
、WriteOnlyCollection.add_all()
和WriteOnlyCollection.remove()
方法。查询集合是通过调用使用WriteOnlyCollection.select()
方法构建的 SELECT 语句来执行的。仅写入加载在仅写入关系中进行了讨论。 - 动态加载 - 可通过
lazy='dynamic'
获得,或通过在Relationship
对象的左侧使用DynamicMapped
注解来标注。这是一种传统的仅限集合的加载样式,当访问集合时会产生一个Query
对象,允许针对集合的内容发出自定义 SQL。然而,动态加载器在各种情况下将隐式迭代底层集合,这使它们对管理真正大型集合不太有用。动态加载器已被“仅写入”集合取代,后者将阻止在任何情况下隐式加载底层集合。动态加载器在动态关系加载器中进行了讨论。
在映射时配置加载策略
特定关系的加载策略可以在映射时配置,以在加载映射类型的对象的所有情况下发生,即使没有修改它的任何查询级别选项。这是使用relationship()
的relationship.lazy
参数进行配置的;此参数的常见值包括select
、selectin
和joined
。
下面的示例说明了在一对多关系模式下的关系示例,配置了Parent.children
关系以在发出Parent
对象的 SELECT 语句时使用选择 IN 加载:
from typing import List from sqlalchemy import ForeignKey from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship class Base(DeclarativeBase): pass class Parent(Base): __tablename__ = "parent" id: Mapped[int] = mapped_column(primary_key=True) children: Mapped[List["Child"]] = relationship(lazy="selectin") class Child(Base): __tablename__ = "child" id: Mapped[int] = mapped_column(primary_key=True) parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
在上面的示例中,每当加载Parent
对象集合时,每个Parent
也将其children
集合填充,使用"selectin"
加载策略发出第二个查询。
relationship.lazy
参数的默认值是"select"
,表示延迟加载。## 使用加载器选项进行关系加载
另一种,可能更常见的配置加载策略的方式是在针对特定属性的每个查询上设置它们,使用Select.options()
方法。使用加载器选项可以对关系加载进行非常详细的控制;最常见的是joinedload()
、selectinload()
和lazyload()
。该选项接受一个类绑定的属性,指示应针对特定类/属性进行定位:
from sqlalchemy import select from sqlalchemy.orm import lazyload # set children to load lazily stmt = select(Parent).options(lazyload(Parent.children)) from sqlalchemy.orm import joinedload # set children to load eagerly with a join stmt = select(Parent).options(joinedload(Parent.children))
加载器选项也可以使用方法链接进行“链接”,以指定进一步深层次的加载方式:
from sqlalchemy import select from sqlalchemy.orm import joinedload stmt = select(Parent).options( joinedload(Parent.children).subqueryload(Child.subelements) )
链接的加载器选项可以应用于“延迟”加载的集合。这意味着当在访问时惰性加载集合或关联时,指定的选项将生效:
from sqlalchemy import select from sqlalchemy.orm import lazyload stmt = select(Parent).options(lazyload(Parent.children).subqueryload(Child.subelements))
在上面的查询中,将返回未加载children
集合的Parent
对象。当首次访问特定Parent
对象上的children
集合时,它将延迟加载相关对象,并且还将对每个children
成员的subelements
集合应用急加载。
向加载器选项添加条件
用于指示加载器选项的关系属性包括向创建的连接的 ON 子句添加额外的过滤条件,或者根据加载器策略涉及到的 WHERE 条件添加过滤条件的能力。这可以通过使用PropComparator.and_()
方法来实现,该方法将通过一个选项传递,以便加载的结果被限制为给定的过滤条件:
from sqlalchemy import select from sqlalchemy.orm import lazyload stmt = select(A).options(lazyload(A.bs.and_(B.id > 5)))
当使用限制条件时,如果特定的集合已经加载,则不会刷新;为了确保新的条件生效,请应用现有填充执行选项:
from sqlalchemy import select from sqlalchemy.orm import lazyload stmt = ( select(A) .options(lazyload(A.bs.and_(B.id > 5))) .execution_options(populate_existing=True) )
为了在查询中添加对实体的所有出现的过滤条件,无论加载策略如何或它在加载过程中的位置如何,参见 with_loader_criteria()
函数。
新版本 1.4 中新增。### 使用 Load.options() 指定子选项
使用方法链,路径中每个链接的加载器样式都明确说明。要沿着路径导航而不更改特定属性的现有加载器样式,请使用 defaultload()
方法/函数:
from sqlalchemy import select from sqlalchemy.orm import defaultload stmt = select(A).options(defaultload(A.atob).joinedload(B.btoc))
类似的方法可以用来一次指定多个子选项,使用 Load.options()
方法:
from sqlalchemy import select from sqlalchemy.orm import defaultload from sqlalchemy.orm import joinedload stmt = select(A).options( defaultload(A.atob).options(joinedload(B.btoc), joinedload(B.btod)) )
另请参见
在相关对象和集合上使用 load_only() - 演示了结合关系和基于列的加载器选项的示例。
注意
应用于对象的延迟加载集合的加载器选项对于特定对象实例是**“粘性的”**,这意味着它们将在内存中存在的对象加载的集合上持续存在。例如,给定前面的示例:
stmt = select(Parent).options(lazyload(Parent.children).subqueryload(Child.subelements))
如果上述查询加载的特定 Parent
对象上的 children
集合过期(例如当 Session
对象的事务提交或回滚时,或使用了 Session.expire_all()
),当下次访问 Parent.children
集合以重新加载时,Child.subelements
集合将再次使用子查询急加载。即使上述 Parent
对象是从指定了不同选项集的后续查询中访问的,这种情况仍然存在。要在不清除对象并重新加载的情况下更改现有对象上的选项,必须使用 Populate Existing 执行选项显式设置它们:
# change the options on Parent objects that were already loaded stmt = ( select(Parent) .execution_options(populate_existing=True) .options(lazyload(Parent.children).lazyload(Child.subelements)) .all() )
如果上面加载的对象被 Session
完全清除,例如由于垃圾回收或使用了 Session.expunge_all()
,那么“粘性”选项也将消失,如果再次加载新创建的对象,则会使用新选项。
未来的 SQLAlchemy 发布版本可能会添加更多的选择来操作已加载对象上的加载器选项。## 延迟加载
默认情况下,所有对象之间的关系都是延迟加载的。与relationship()
关联的标量或集合属性包含一个触发器,在第一次访问属性时触发。这个触发器通常在访问点发出 SQL 调用,以加载相关的对象或对象:
>>> spongebob.addresses 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 [5] [<Address(u'spongebob@google.com')>, <Address(u'j25@yahoo.com')>]
唯一的情况是不发出 SQL 的情况是简单的多对一关系,当相关对象仅可以通过其主键单独标识,并且该对象已经存在于当前Session
中时。因此,虽然对相关集合进行延迟加载可能很昂贵,但在加载许多对象与相对较小的可能目标对象集合的情况下,延迟加载可能能够在本地引用这些对象,而无需发出与父对象数量相同数量的 SELECT 语句。
这种“根据属性访问加载”的默认行为称为“延迟”或“选择”加载 - 名称“选择”是因为在首次访问属性时通常会发出“SELECT”语句。
可以使用lazyload()
加载器选项来启用通常以其他方式配置的给定属性的延迟加载:
from sqlalchemy import select from sqlalchemy.orm import lazyload # force lazy loading for an attribute that is set to # load some other way normally stmt = select(User).options(lazyload(User.addresses))
使用 raiseload 防止不需要的延迟加载
lazyload()
策略产生的效果是对象关系映射中最常见的问题之一;N 加一问题,它指出对于任何加载的 N 个对象,访问其惰性加载的属性意味着会发出 N+1 个 SELECT 语句。在 SQLAlchemy 中,解决 N+1 问题的通常方法是利用其非常强大的急加载系统。然而,急加载要求事先使用Select
指定要加载的属性。可能访问其他未急加载的属性的代码问题,不希望延迟加载,可以使用raiseload()
策略来解决;这个加载器策略用引发一个具有信息性错误的方式替换了惰性加载的行为:
from sqlalchemy import select from sqlalchemy.orm import raiseload stmt = select(User).options(raiseload(User.addresses))
以上,从上述查询加载的User
对象将不会加载.addresses
集合;如果稍后的一些代码尝试访问此属性,则会引发 ORM 异常。
可以使用所谓的“通配符”指示符将raiseload()
用于表示所有关系都应该使用这种策略。例如,设置仅一个属性为急加载,并将其余全部设置为 raise:
from sqlalchemy import select from sqlalchemy.orm import joinedload from sqlalchemy.orm import raiseload stmt = select(Order).options(joinedload(Order.items), raiseload("*"))
上述通配符将适用于所有关系,而不仅适用于items
,还适用于Item
对象上的所有关系。要仅为Order
对象设置raiseload()
,请指定具有Load
的完整路径:
from sqlalchemy import select from sqlalchemy.orm import joinedload from sqlalchemy.orm import Load stmt = select(Order).options(joinedload(Order.items), Load(Order).raiseload("*"))
相反,要为仅Item
对象设置提升:
stmt = select(Order).options(joinedload(Order.items).raiseload("*"))
raiseload()
选项仅适用于关系属性。对于基于列的属性,defer()
选项支持defer.raiseload
选项,其工作方式相同。
提示
“raiseload”策略在工作单元刷新过程中不适用。这意味着如果Session.flush()
过程需要加载集合以完成其工作,则会在绕过任何raiseload()
指令的情况下执行此操作。
另请参见
通配符加载策略
使用 raiseload 防止延迟列加载 ## 连接预加载
连接预加载是包含在 SQLAlchemy ORM 中的最古老的预加载样式。它通过将 JOIN(默认为 LEFT OUTER join)连接到发出的 SELECT 语句,并且从与父级相同的结果集中填充目标标量/集合来工作。
在映射级别,看起来像这样:
class Address(Base): # ... user: Mapped[User] = relationship(lazy="joined")
连接预加载通常作为查询的选项应用,而不是作为映射的默认加载选项,特别是在用于集合而不是多对一引用时。可以使用joinedload()
加载器选项来实现这一点:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import joinedload >>> stmt = select(User).options(joinedload(User.addresses)).filter_by(name="spongebob") >>> spongebob = 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 = ? ['spongebob']
提示
当参考一对多或多对多集合时包括joinedload()
时,必须对返回的结果应用Result.unique()
方法,该方法将通过否则由连接相乘出的主键使传入的行不重复。如果未提供此项,则 ORM 将引发错误。
这在现代 SQLAlchemy 中不是自动的,因为它会更改结果集的行为,使其返回的 ORM 对象比语句通常返回的行数少。因此,SQLAlchemy 保持了对Result.unique()
的使用是显式的,因此返回的对象是在主键上唯一的,没有任何歧义。
默认情况下发出的 JOIN 是 LEFT OUTER JOIN,以允许不引用相关行的主对象。对于保证具有元素的属性,例如引用相关对象的多对一引用,其中引用外键不为 NULL,可以通过使用内连接使查询更有效;这在映射级别通过 relationship.innerjoin
标志可用:
class Address(Base): # ... user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) user: Mapped[User] = relationship(lazy="joined", innerjoin=True)
在查询选项级别,通过 joinedload.innerjoin
标志:
from sqlalchemy import select from sqlalchemy.orm import joinedload stmt = select(Address).options(joinedload(Address.user, innerjoin=True))
当应用于包含 OUTER JOIN 的链时,JOIN 会向右嵌套自身:
>>> from sqlalchemy import select >>> from sqlalchemy.orm import joinedload >>> stmt = select(User).options( ... joinedload(User.addresses).joinedload(Address.widgets, innerjoin=True) ... ) >>> results = session.scalars(stmt).unique().all() SELECT widgets_1.id AS widgets_1_id, widgets_1.name AS widgets_1_name, 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 JOIN widgets AS widgets_1 ON addresses_1.widget_id = widgets_1.id ) ON users.id = addresses_1.user_id
提示
如果在发出 SELECT
时使用数据库行锁定技术,即意味着正在使用 Select.with_for_update()
方法发出 SELECT..FOR UPDATE
,则根据使用的后端的行为,可能会锁定连接的表。出于这个原因,不建议同时使用连接的急切加载和 SELECT..FOR UPDATE
。
SqlAlchemy 2.0 中文文档(十九)(2)https://developer.aliyun.com/article/1562931