级联

映射器支持在 relationship() 结构上配置 级联 行为的概念。这指的是对相对于特定 Session 的“父”对象执行的操作如何传播到该关系引用的项(例如,“子”对象),并且受 relationship.cascade 选项影响。

级联的默认行为仅限于所谓的 save-updatemerge 设置的级联。级联的典型“替代”设置是添加 deletedelete-orphan 选项;这些设置适用于仅在与父级关联时才存在的相关对象,否则将被删除。

级联行为使用 relationship.cascade 选项在 relationship() 中配置。

class Order(Base):
    __tablename__ = "order"

    items = relationship("Item", cascade="all, delete-orphan")
    customer = relationship("User", cascade="save-update")

要在 backref 上设置级联,可以使用相同的标志与 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。符号 allsave-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,它有两个相关对象 address1address2

>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]

如果我们将 user1 添加到 Session 中,它也会隐式添加 address1address2

>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True

save-update 级联还会影响已存在于 Session 中的对象的属性操作。如果我们将第三个对象 address3 添加到 user1.addresses 集合中,它将成为该 Session 状态的一部分。

>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True

当从集合中删除项目或从标量属性中取消关联对象时,save-update 级联可能会表现出令人惊讶的行为。在某些情况下,孤立的对象可能仍会拉入前父级的 Session 中;这样做是为了使 flush 过程能够适当地处理该相关对象。这种情况通常只发生在从一个 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
True

save-update 级联默认启用,通常被视为理所当然;它通过允许使用单次对 Session.add() 的调用来立即在该 Session 中注册整个对象结构,从而简化代码。虽然可以禁用它,但通常没有必要这样做。

双向关系中 save-update 级联的行为

在双向关系的上下文中,save-update 级联是单向的,即在使用 relationship.back_populatesrelationship.backref 参数创建两个分别引用彼此的 relationship() 对象时。

当一个未与 Session 关联的对象被分配到一个与 Session 关联的父对象的属性或集合中时,该对象将自动添加到同一个 Session 中。但是,相反的操作不会产生这种效果;一个未与 Session 关联的对象,在其被分配了与 Session 关联的子对象后,不会自动将该父对象添加到 Session 中。这种行为的总体主题被称为“级联反向引用”,它代表了从 SQLAlchemy 2.0 开始标准化的行为变化。

为了说明,假设一个 Order 对象的映射,它们通过关系 Order.itemsItem.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 对象并将其追加到该 OrderOrder.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.itemsItem.order 的双向性质意味着追加到 Order.items 也意味着分配到 Item.order。同时,允许 Item 对象添加到与父 Order 关联的同一个 Session 中的 save-update 级联。

但是,如果上面的操作以 **相反** 方向执行,其中 Item.order 被分配而不是直接追加到 Order.item,即使对象分配 Order.itemsItem.order 处于与先前示例相同的状态,级联操作到 Session 也不会自动发生。

>>> 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 为删除,在刷新操作完成之后,address1address2 也将被删除

>>> 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 的默认行为是通过将其外键引用设置为 NULL 来将 address1address2user1 解除关联。使用以下映射

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。 deletedelete-orphan 级联的组合涵盖了 SQLAlchemy 必须在设置外键列为 NULL 与完全删除行之间做出决定的两种情况。

默认情况下,该功能完全独立于数据库配置的 FOREIGN KEY 约束,这些约束本身可能配置 CASCADE 行为。为了更有效地与这种配置集成,应该使用在 使用外键 ON DELETE 级联与 ORM 关系 中描述的其他指令。

警告

请注意,ORM 的“delete”和“delete-orphan”行为 **仅** 适用于使用 Session.delete() 方法在 工作单元 过程中标记单个 ORM 实例进行删除。它 **不** 适用于“批量”删除,批量删除将使用 delete() 结构发出,如 使用自定义 WHERE 条件的 ORM UPDATE 和 DELETE 所示。有关更多背景信息,请参阅 ORM 启用更新和删除的重要说明和注意事项

在多对多关系中使用删除级联

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"设置在**两个**关系中都被配置,那么级联操作将继续级联所有ParentChild对象,加载遇到的每个childrenparents集合,并删除所有连接的对象。通常不希望“delete”级联双向配置。

在 ORM 关系中使用外键 ON DELETE 级联

SQLAlchemy 的“delete”级联的行为与数据库FOREIGN KEY约束的ON DELETE功能重叠。SQLAlchemy 允许使用DDL构造的ForeignKeyForeignKeyConstraint来配置这些模式级行为;这些对象与Table元数据的结合使用在ON UPDATE and ON DELETE中描述。

为了在relationship()中使用ON DELETE外键级联,首先要注意的是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")

当父行被删除时上述配置的行为如下

  1. 应用程序调用session.delete(my_parent),其中my_parentParent的实例。

  2. Session下次将更改刷新到数据库时,ORM 将删除my_parent.children集合中的所有当前加载的项目,这意味着将为每个记录发出DELETE语句。

  3. 如果my_parent.children集合被卸载,则不会发出任何DELETE语句。如果在这个relationship()没有设置relationship.passive_deletes标志,那么将为卸载的Child对象发出SELECT语句。

  4. 然后,为my_parent行本身发出DELETE语句。

  5. 数据库级ON DELETE CASCADE设置确保child中所有引用parent中受影响行的行也被删除。

  6. my_parent引用的Parent实例,以及与该对象相关的所有已加载的Child实例(即上面的步骤 2 已经完成),都将与Session分离。

注意

要使用“ON DELETE CASCADE”,底层数据库引擎必须支持FOREIGN KEY约束,并且它们必须在执行中

在多对多关系中使用外键 ON 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 对象的删除过程如下

  1. 使用 Session.delete()Parent 对象标记为删除。

  2. 当发生刷新时,如果 Parent.children 集合未加载,ORM 将首先发出 SELECT 语句,以加载对应于 Parent.childrenChild 对象。

  3. 然后它将为 association 中对应于该父级行的行发出 DELETE 语句。

  4. 对于受此立即删除影响的每个 Child 对象,因为配置了 passive_deletes=True,工作单元不需要尝试为每个 Child.parents 集合发出 SELECT 语句,因为它假设 association 中的对应行将被删除。

  5. 然后为从 Parent.children 加载的每个 Child 对象发出 DELETE 语句。

delete-orphan

delete-orphan 级联在 delete 级联中添加了行为,这样当子对象与父对象分离时,子对象将被标记为删除,而不仅仅是在父对象被标记为删除时。当处理由其父对象“拥有”的关联对象时,这是一种常见的功能,该关联对象具有一个 NOT NULL 外键,因此从父对象集合中删除该项目会导致该项目的删除。

delete-orphan 级联意味着每个子对象一次只能有一个父对象,并且在绝大多数情况下,它仅在“一对多”关系上配置。对于在多对一或多对多关系上设置它的非常不常见的情况,可以强制“多”端一次只允许一个对象,方法是配置 relationship.single_parent 参数,它建立 Python 端验证以确保该对象一次只与一个父对象相关联,但这极大地限制了“多”关系的功能,通常不是想要的。

merge

merge 级联表示 Session.merge() 操作应该从作为 Session.merge() 调用的主题的父对象传播到引用的对象。此级联在默认情况下也是开启的。

refresh-expire

refresh-expire 是一个不常见的选项,表示 Session.expire() 操作应该从父对象传播到引用的对象。当使用 Session.refresh() 时,引用的对象仅过期,但不会真正刷新。

expunge

expunge 级联表示当父对象使用 Session.expunge()Session 中删除时,该操作应该传播到引用的对象。

关于删除 - 删除从集合和标量关系引用的对象

ORM 通常不会在刷新过程中修改集合或标量关系的内容。这意味着,如果你的类具有一个 relationship(),它引用对象集合,或者引用单个对象(例如多对一),那么当刷新过程发生时,该属性的内容将不会被修改。相反,预计 Session 最终会过期,无论是通过 Session.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

另请参阅

级联 以了解级联的详细信息。