SQLAlchemy 2.0 文档
级联¶
映射器支持在 relationship() 构造上配置 级联 行为的概念。 这指的是相对于特定 Session 在“父”对象上执行的操作应如何传播到该关系引用的项(例如“子”对象),并受 relationship.cascade 选项的影响。
级联的默认行为仅限于所谓的 save-update 和 merge 设置的级联。 级联的典型“替代”设置是添加 delete 和 delete-orphan 选项; 这些设置适用于仅在其连接到父对象时才存在的关联对象,否则将被删除。
级联行为使用 relationship.cascade 选项在 relationship() 上配置
class Order(Base):
__tablename__ = "order"
items = relationship("Item", cascade="all, delete-orphan")
customer = relationship("User", cascade="save-update")要在反向引用上设置级联,可以将相同的标志与 backref() 函数一起使用,该函数最终将其参数反馈到 relationship() 中
class Item(Base):
__tablename__ = "item"
order = relationship(
"Order", backref=backref("items", cascade="all, delete-orphan")
)relationship.cascade 的默认值为 save-update, merge。 此参数的典型替代设置是 all 或更常见的 all, delete-orphan。 all 符号是 save-update, merge, refresh-expire, expunge, delete 的同义词,将其与 delete-orphan 结合使用表示子对象应在所有情况下都跟随其父对象,并在不再与该父对象关联时被删除。
警告
all 级联选项暗示了 refresh-expire 级联设置,当使用 异步 I/O (asyncio) 扩展时,这可能不是理想的,因为它会比在显式 IO 上下文中通常更积极地使关联对象过期。 有关更多背景信息,请参阅 使用 AsyncSession 时防止隐式 IO 中的注释。
可以在 relationship.cascade 参数中指定的可用值列表在以下小节中描述。
save-update¶
save-update 级联表示当通过 Session.add() 将对象放入 Session 时,所有通过此 relationship() 与之关联的对象也应添加到同一 Session 中。 假设我们有一个对象 user1,它有两个关联的对象 address1, address2
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]如果我们添加 user1 到 Session,它也会隐式地添加 address1, address2
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
Truesave-update 级联也会影响已存在于 Session 中的对象的属性操作。 如果我们向 user1.addresses 集合添加第三个对象 address3,它将成为该 Session 状态的一部分
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True当从集合中删除项目或取消对象与标量属性的关联时,save-update 级联可能会表现出令人惊讶的行为。 在某些情况下,孤立的对象可能仍会被拉入前父对象的 Session; 这是为了使刷新过程可以适当地处理该关联对象。 这种情况通常仅在对象从一个 Session 中删除并添加到另一个 Session 时发生
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1 = user1.addresses[0]
>>> sess1.close() # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1) # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1) # ... but it still gets added to the new session,
>>> address1 in sess2 # because it's still "pending" for flush
Truesave-update 级联默认开启,并且通常被认为是理所当然的; 它简化了代码,允许对 Session.add() 的单次调用,即可在一个 Session 中注册整个对象结构。 虽然可以禁用它,但通常没有必要这样做。
双向关系中 save-update 级联的行为¶
在双向关系的上下文中,save-update 级联是 单向 的,即当使用 relationship.back_populates 或 relationship.backref 参数创建两个相互引用的单独的 relationship() 对象时。
未与 Session 关联的对象,当分配给与 Session 关联的父对象上的属性或集合时,将自动添加到同一 Session。 但是,反向操作不会产生这种效果; 未与 Session 关联的对象,在其上分配了与 Session 关联的子对象时,不会导致自动将该父对象添加到 Session。 此行为的总体主题被称为“级联反向引用”,代表了 SQLAlchemy 2.0 标准化的行为更改。
为了说明,给定 Order 对象的映射,这些对象通过关系 Order.items 和 Item.order 双向关联到一系列 Item 对象
mapper_registry.map_imperatively(
Order,
order_table,
properties={"items": relationship(Item, back_populates="order")},
)
mapper_registry.map_imperatively(
Item,
item_table,
properties={"order": relationship(Order, back_populates="items")},
)如果 Order 已经与 Session 关联,并且然后创建了一个 Item 对象并将其附加到该 Order 的 Order.items 集合中,则 Item 将自动级联到同一 Session 中
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True
>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True以上,Order.items 和 Item.order 的双向性质意味着附加到 Order.items 也会分配给 Item.order。 同时,save-update 级联允许将 Item 对象添加到父 Order 已经关联的同一 Session 中。
但是,如果上述操作以 相反 的方向执行,其中分配了 Item.order 而不是直接附加到 Order.item,则到 Session 的级联操作将 不会 自动发生,即使对象分配 Order.items 和 Item.order 将与上一个示例中的状态相同
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True
>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False在上述情况下,在创建 Item 对象并对其设置所有所需状态后,应将其显式添加到 Session 中
>>> session.add(i1)在较旧版本的 SQLAlchemy 中,save-update 级联将在所有情况下双向发生。 然后,使用一个名为 cascade_backrefs 的选项使其成为可选的。 最后,在 SQLAlchemy 1.4 中,旧的行为已被弃用,并且 cascade_backrefs 选项在 SQLAlchemy 2.0 中被删除。 理由是用户通常不认为分配给对象上的属性(如上例所示的 i1.order = o1 的分配)会改变该对象 i1 的持久性状态,使其现在在 Session 中处于挂起状态,并且在给定对象仍在构建且未处于准备好刷新的状态的情况下,自动刷新经常会过早刷新对象并导致错误。 选择单向和双向行为的选项也被删除,因为此选项创建了两种略有不同的工作方式,增加了 ORM 的总体学习曲线以及文档和用户支持负担。
另请参阅
cascade_backrefs 行为已弃用,将在 2.0 中删除 - 关于“级联反向引用”行为更改的背景
delete¶
delete 级联表示当“父”对象被标记为删除时,其相关的“子”对象也应被标记为删除。 例如,如果我们有一个关系 User.addresses 配置了 delete 级联
class User(Base):
# ...
addresses = relationship("Address", cascade="all, delete")如果使用上述映射,我们有一个 User 对象和两个相关的 Address 对象
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses如果我们标记 user1 为删除,在刷新操作继续进行后,address1 和 address2 也将被删除
>>> sess.delete(user1)
>>> sess.commit()
DELETE FROM address WHERE address.id = ?
((1,), (2,))
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
或者,如果我们的 User.addresses 关系不具有 delete 级联,SQLAlchemy 的默认行为是将 address1 和 address2 从 user1 中取消关联,方法是将其外键引用设置为 NULL。 使用以下映射
class User(Base):
# ...
addresses = relationship("Address")在删除父 User 对象后,address 中的行不会被删除,而是被取消关联
>>> sess.delete(user1)
>>> sess.commit()
UPDATE address SET user_id=? WHERE address.id = ?
(None, 1)
UPDATE address SET user_id=? WHERE address.id = ?
(None, 2)
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
一对多关系上的 delete 级联通常与 delete-orphan 级联结合使用,如果“子”对象从父对象取消关联,则将为关联行发出 DELETE。 delete 和 delete-orphan 级联的组合涵盖了 SQLAlchemy 必须在将外键列设置为 NULL 还是完全删除行之间做出决定的两种情况。
默认情况下,此功能完全独立于数据库配置的 FOREIGN KEY 约束工作,这些约束本身可能会配置 CASCADE 行为。 为了更有效地与此配置集成,应使用 将外键 ON DELETE 级联与 ORM 关系一起使用 中描述的其他指令。
警告
请注意,ORM 的“delete”和“delete-orphan”行为仅适用于使用 Session.delete() 方法在 工作单元 进程中标记要删除的单个 ORM 实例。 它 不 适用于“批量”删除,批量删除将使用 delete() 构造发出,如 使用自定义 WHERE 条件的 ORM UPDATE 和 DELETE 中所示。 有关更多背景信息,请参阅 ORM 启用更新和删除的重要注意事项和警告。
在多对多关系中使用 delete 级联¶
cascade="all, delete" 选项在多对多关系中也同样有效,多对多关系使用 relationship.secondary 来指示关联表。 当父对象被删除,因此与其关联的对象取消关联时,工作单元进程通常会删除关联表中的行,但保持关联对象完整。 当与 cascade="all, delete" 结合使用时,将为子行本身执行额外的 DELETE 语句。
以下示例改编自 多对多,以说明关联的 一侧 的 cascade="all, delete" 设置
association_table = Table(
"association",
Base.metadata,
Column("left_id", Integer, ForeignKey("left.id")),
Column("right_id", Integer, ForeignKey("right.id")),
)
class Parent(Base):
__tablename__ = "left"
id = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = mapped_column(Integer, primary_key=True)
parents = relationship(
"Parent",
secondary=association_table,
back_populates="children",
)以上,当使用 Session.delete() 将 Parent 对象标记为删除时,刷新过程将像往常一样删除 association 表中的关联行,但是根据级联规则,它还将删除所有相关的 Child 行。
警告
如果上述 cascade="all, delete" 设置在 两个 关系上都配置了,那么级联操作将继续级联所有 Parent 和 Child 对象,加载遇到的每个 children 和 parents 集合,并删除所有连接的内容。 通常不希望双向配置“delete”级联。
将外键 ON DELETE 级联与 ORM 关系一起使用¶
SQLAlchemy 的“delete”级联的行为与数据库 FOREIGN KEY 约束的 ON DELETE 功能重叠。 SQLAlchemy 允许使用 ForeignKey 和 ForeignKeyConstraint 构造配置这些模式级 DDL 行为; Table 元数据结合使用这些对象的描述见 ON UPDATE 和 ON DELETE。
为了将 ON DELETE 外键级联与 relationship() 结合使用,首先需要注意的是,relationship.cascade 设置仍然必须配置为匹配所需的“delete”或“set null”行为(使用 delete 级联或将其省略),以便无论 ORM 还是数据库级约束将处理实际修改数据库中数据的任务,ORM 仍然能够适当地跟踪可能受影响的本地存在对象的状态。
然后,relationship() 上还有一个额外的选项,指示 ORM 应在多大程度上尝试自行对相关行运行 DELETE/UPDATE 操作,以及它应该在多大程度上依赖于数据库端 FOREIGN KEY 约束级联来处理任务; 这是 relationship.passive_deletes 参数,它接受选项 False (默认值)、True 和 "all"。
最典型的示例是当父行被删除时,子行也将被删除,并且在相关的 FOREIGN KEY 约束上也配置了 ON DELETE CASCADE
class Parent(Base):
__tablename__ = "parent"
id = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
back_populates="parent",
cascade="all, delete",
passive_deletes=True,
)
class Child(Base):
__tablename__ = "child"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
parent = relationship("Parent", back_populates="children")当父行被删除时,上述配置的行为如下
应用程序调用
session.delete(my_parent),其中my_parent是Parent的实例。当
Session下次将更改刷新到数据库时,my_parent.children集合中所有 当前加载的 项都将由 ORM 删除,这意味着将为每个记录发出DELETE语句。如果
my_parent.children集合是 未加载的,则不会发出DELETE语句。 如果relationship.passive_deletes标志 未 在此relationship()上设置,则将为未加载的Child对象发出SELECT语句。然后,为
my_parent行本身发出DELETE语句。数据库级
ON DELETE CASCADE设置确保child中引用parent中受影响行的所有行也被删除。由
my_parent引用的Parent实例,以及与此对象相关且 已加载 的所有Child实例(即执行了上面的步骤 2),都将从Session中取消关联。
注意
要使用 “ON DELETE CASCADE”,底层数据库引擎必须支持 FOREIGN KEY 约束,并且它们必须是强制执行的
当使用 MySQL 时,必须选择合适的存储引擎。 有关详细信息,请参阅 CREATE TABLE 参数,包括存储引擎。
当使用 SQLite 时,必须显式启用外键支持。 有关详细信息,请参阅 外键支持。
将外键 ON DELETE 与多对多关系一起使用¶
如 将删除级联与多对多关系一起使用 中所述,“delete” 级联也适用于多对多关系。为了结合多对多关系使用 ON DELETE CASCADE 外键,需要在关联表上配置 FOREIGN KEY 指令。这些指令可以处理自动从关联表中删除的任务,但无法处理自动删除相关对象本身。
在这种情况下,relationship.passive_deletes 指令可以为我们节省一些额外的 SELECT 语句,在删除操作期间,但 ORM 仍然会继续加载一些集合,以便找到受影响的子对象并正确处理它们。
注意
对此的假设性优化可能包括对关联表的所有父级关联行执行单个 DELETE 语句,然后使用 RETURNING 来定位受影响的相关子行,但这目前不是 ORM 工作单元实现的一部分。
在此配置中,我们在关联表的两个外键约束上都配置了 ON DELETE CASCADE。我们在父级->子级关系的一侧配置了 cascade="all, delete",然后我们可以在双向关系的另一侧配置 passive_deletes=True,如下所示
association_table = Table(
"association",
Base.metadata,
Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)
class Parent(Base):
__tablename__ = "left"
id = mapped_column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
back_populates="parents",
cascade="all, delete",
)
class Child(Base):
__tablename__ = "right"
id = mapped_column(Integer, primary_key=True)
parents = relationship(
"Parent",
secondary=association_table,
back_populates="children",
passive_deletes=True,
)使用上述配置,删除 Parent 对象的过程如下
使用
Session.delete()将Parent对象标记为待删除。当刷新发生时,如果
Parent.children集合未加载,ORM 将首先发出一个 SELECT 语句,以便加载与Parent.children对应的Child对象。然后,它将为
association中与该父行对应的行发出DELETE语句。对于受此立即删除影响的每个
Child对象,由于配置了passive_deletes=True,工作单元将不需要尝试为每个Child.parents集合发出 SELECT 语句,因为它假定association中的相应行将被删除。然后,为从
Parent.children加载的每个Child对象发出DELETE语句。
delete-orphan¶
delete-orphan 级联为 delete 级联添加了行为,以便在子对象从父对象解除关联时(而不仅仅是在父对象被标记为删除时)将其标记为删除。当处理由其父对象“拥有”的相关对象(具有 NOT NULL 外键)时,这是一个常见的功能,因此从父对象集合中删除该项会导致其删除。
delete-orphan 级联意味着每个子对象一次只能有一个父对象,并且在绝大多数情况下仅在 一对多关系上配置。 对于在多对一或多对多关系上设置它的不太常见的情况,“多”方可以被强制为一次只允许一个对象,通过配置 relationship.single_parent 参数,该参数建立 Python 端的验证,以确保对象一次仅与一个父对象关联,但这大大限制了“多”关系的功用,通常不是期望的结果。
另请参阅
对于关系 <relationship>,delete-orphan 级联通常仅在一对多关系的“一”方配置,而不是在多对一或多对多关系的“多”方配置。 - 关于涉及 delete-orphan 级联的常见错误场景的背景信息。
merge¶
merge 级联指示 Session.merge() 操作应从作为 Session.merge() 调用的主体的父对象传播到引用的对象。此级联默认也是开启的。
refresh-expire¶
refresh-expire 是一个不常见的选项,指示 Session.expire() 操作应从父对象传播到引用的对象。当使用 Session.refresh() 时,引用的对象仅过期,但实际上并未刷新。
expunge¶
expunge 级联指示当使用 Session.expunge() 从 Session 中删除父对象时,该操作应向下传播到引用的对象。
关于删除的注意事项 - 删除从集合和标量关系引用的对象¶
通常,ORM 在刷新过程中永远不会修改集合或标量关系的内容。这意味着,如果您的类具有 relationship(),该关系引用对象集合,或者引用单个对象(例如多对一),则在刷新过程发生时,此属性的内容不会被修改。相反,期望 Session 最终会过期,无论是通过 Session.commit() 的 expire-on-commit 行为,还是通过显式使用 Session.expire()。届时,与该 Session 关联的任何引用的对象或集合都将被清除,并在下次访问时重新加载自身。
关于此行为的常见混淆涉及 Session.delete() 方法的使用。当在对象上调用 Session.delete() 并且 Session 被刷新时,该行将从数据库中删除。通过外键引用目标行的行,假设它们使用两个映射对象类型之间的 relationship() 进行跟踪,也将看到它们的外键属性被更新为 null,或者如果设置了删除级联,则相关行也将被删除。但是,即使与已删除对象相关的行本身也可能被修改,在刷新本身的范围内,对操作中涉及的对象的关系绑定集合或对象引用也不会发生更改。这意味着,如果该对象是相关集合的成员,它仍将存在于 Python 端,直到该集合过期。类似地,如果该对象通过多对一或一对一从另一个对象引用,则该引用将保留在该对象上,直到该对象也过期为止。
下面,我们说明在 Address 对象被标记为删除后,它仍然存在于与父 User 关联的集合中,即使在刷新之后也是如此
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True当提交上述会话时,所有属性都将过期。下次访问 user.addresses 将重新加载集合,显示所需的状态
>>> session.commit()
>>> address in user.addresses
False有一种配方可以拦截 Session.delete() 并自动调用此过期;请参阅 ExpireRelationshipOnFKChange 以了解这一点。但是,在集合中删除项目的常用做法是放弃直接使用 Session.delete(),而是使用级联行为来自动调用删除,作为从父集合中删除对象的结果。delete-orphan 级联实现了这一点,如下例所示
class User(Base):
__tablename__ = "user"
# ...
addresses = relationship("Address", cascade="all, delete-orphan")
# ...
del user.addresses[1]
session.flush()在上面,当从 User.addresses 集合中删除 Address 对象时,delete-orphan 级联的效果是将 Address 对象标记为删除,方式与将其传递给 Session.delete() 相同。
delete-orphan 级联也可以应用于多对一或一对一关系,以便在对象从其父对象解除关联时,它也会自动标记为删除。在多对一或一对一关系上使用 delete-orphan 级联需要一个额外的标志 relationship.single_parent,该标志会调用一个断言,即此相关对象不与其他任何父对象同时共享
class User(Base):
# ...
preference = relationship(
"Preference", cascade="all, delete-orphan", single_parent=True
)在上面,如果假设的 Preference 对象从 User 中删除,它将在刷新时被删除
some_user.preference = None
session.flush() # will delete the Preference object另请参阅
级联 有关级联的详细信息。
flambé! 龙和 The Alchemist 图像设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档最后生成时间:Tue 11 Mar 2025 02:40:17 PM EDT