级联

Cascades

Mappers 支持在 relationship() 构造上配置 cascade (级联)行为的概念。 这指的是在一个“父”对象上执行的操作应如何相对于某个特定的 Session 传播到该关系引用的项目(例如“子”对象), 其行为受 relationship.cascade 选项的影响。

级联的默认行为仅限于所谓的 保存-更新合并 设置。 级联的典型“替代”设置是添加 删除删除-孤立 选项; 这些设置适用于那些只要附属于其父对象就存在,否则就应被删除的相关对象。

级联行为通过 relationship() 上的 relationship.cascade 选项进行配置:

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 级联选项隐含了 刷新-过期 设置, 在使用 异步 I/O (asyncio) 扩展时可能不太理想, 因为它会比通常在显式 IO 场景中更积极地使相关对象过期。 更多背景信息可参见 使用 AsyncSession 时防止隐式 IO 中的说明。

relationship.cascade 参数可用的取值将在下列小节中介绍。

Mappers support the concept of configurable cascade behavior on relationship() constructs. This refers to how operations performed on a “parent” object relative to a particular Session should be propagated to items referred to by that relationship (e.g. “child” objects), and is affected by the relationship.cascade option.

The default behavior of cascade is limited to cascades of the so-called 保存-更新 and 合并 settings. The typical “alternative” setting for cascade is to add the 删除 and 删除-孤立 options; these settings are appropriate for related objects which only exist as long as they are attached to their parent, and are otherwise deleted.

Cascade behavior is configured using the relationship.cascade option on relationship():

class Order(Base):
    __tablename__ = "order"

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

To set cascades on a backref, the same flag can be used with the backref() function, which ultimately feeds its arguments back into relationship():

class Item(Base):
    __tablename__ = "item"

    order = relationship(
        "Order", backref=backref("items", cascade="all, delete-orphan")
    )

The default value of relationship.cascade is save-update, merge. The typical alternative setting for this parameter is either all or more commonly all, delete-orphan. The all symbol is a synonym for save-update, merge, refresh-expire, expunge, delete, and using it in conjunction with delete-orphan indicates that the child object should follow along with its parent in all cases, and be deleted once it is no longer associated with that parent.

警告

The all cascade option implies the 刷新-过期 cascade setting which may not be desirable when using the 异步 I/O (asyncio) extension, as it will expire related objects more aggressively than is typically appropriate in an explicit IO context. See the notes at 使用 AsyncSession 时防止隐式 IO for further background.

The list of available values which can be specified for the relationship.cascade parameter are described in the following subsections.

保存-更新

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 中移除后又添加到另一个中时发生:

>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1 = user1.addresses[0]
>>> sess1.close()  # user1 和 address1 不再与 sess1 关联
>>> user1.addresses.remove(address1)  # address1 不再与 user1 关联
>>> sess2 = Session()
>>> sess2.add(user1)  # ... 但 address1 仍会被添加到新会话中,
>>> address1 in sess2  # 因为它仍然处于“等待 flush”的状态
True

save-update 级联默认启用,通常会被默认接受;它通过允许一次 Session.add() 调用就能将整个对象结构注册到 Session 中,从而简化了代码。 虽然它是可以被禁用的,但通常没有必要这样做。

save-update cascade indicates that when an object is placed into a Session via Session.add(), all the objects associated with it via this relationship() should also be added to that same Session. Suppose we have an object user1 with two related objects address1, address2:

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

If we add user1 to a Session, it will also add address1, address2 implicitly:

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

save-update cascade also affects attribute operations for objects that are already present in a Session. If we add a third object, address3 to the user1.addresses collection, it becomes part of the state of that Session:

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

A save-update cascade can exhibit surprising behavior when removing an item from a collection or de-associating an object from a scalar attribute. In some cases, the orphaned objects may still be pulled into the ex-parent’s Session; this is so that the flush process may handle that related object appropriately. This case usually only arises if an object is removed from one Session and added to another:

>>> 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

The save-update cascade is on by default, and is typically taken for granted; it simplifies code by allowing a single call to Session.add() to register an entire structure of objects within that Session at once. While it can be disabled, there is usually not a need to do so.

具有双向关系的保存-更新级联的行为

Behavior of save-update cascade with bi-directional relationships

在双向关系的上下文中, save-update 级联是 单向 发生的, 即当使用 relationship.back_populatesrelationship.backref 参数创建两个互相关联的 relationship() 对象时。

如果一个未与 Session 关联的对象被分配给一个已与 Session 关联的父对象的属性或集合, 该对象会被自动添加到相同的 Session 中。 然而,反过来的操作则不会有这种效果; 即当一个已与 Session 关联的子对象被分配给一个未与任何 Session 关联的父对象时, 不会导致该父对象自动添加到该 Session 中。 此行为被称为“级联反向引用(cascade backrefs)”, 该行为在 SQLAlchemy 2.0 中被标准化并发生了变化。

为了说明这个问题,假设有 Order 对象和一组 Item 对象之间的双向关系, 通过 Order.itemsItem.order 两个关系字段建立:

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 对象并将其追加到该 Orderitems 集合中, 那么该 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。 与此同时,save-update 级联使得该 Item 对象被加入到 Order 所属的 Session 中。

然而,如果上述操作以 相反方向 进行,即直接给 Item.order 赋值而不是添加到 Order.items, 那么级联操作将 不会 自动发生, 即使最终 Order.itemsItem.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 中此旧行为被弃用, 而在 SQLAlchemy 2.0 中 cascade_backrefs 选项被彻底移除。 其背后的理由是: 用户通常不会觉得直观——即为某对象的属性赋值(如 i1.order = o1)竟然会影响对象 i1 的持久化状态, 使得它变成了一个挂起(pending)状态的对象; 这在后续过程中容易引发问题,例如在 autoflush 中对象被过早 flush, 而此时该对象可能仍在构建中,尚未准备好被 flush。 此外,允许选择单向或双向行为的选项也被移除, 因为这会导致 ORM 中出现两种略有不同的行为方式, 从而增加了学习曲线、文档复杂度以及用户支持的负担。

参见

cascade_backrefs behavior deprecated for removal in 2.0 - 有关“级联反向引用”行为更改的背景资料

The save-update cascade takes place uni-directionally in the context of a bi-directional relationship, i.e. when using the relationship.back_populates or relationship.backref parameters to create two separate relationship() objects which refer to each other.

An object that’s not associated with a Session, when assigned to an attribute or collection on a parent object that is associated with a Session, will be automatically added to that same Session. However, the same operation in reverse will not have this effect; an object that’s not associated with a Session, upon which a child object that is associated with a Session is assigned, will not result in an automatic addition of that parent object to the Session. The overall subject of this behavior is known as “cascade backrefs”, and represents a change in behavior that was standardized as of SQLAlchemy 2.0.

To illustrate, given a mapping of Order objects which relate bi-directionally to a series of Item objects via relationships Order.items and Item.order:

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")},
)

If an Order is already associated with a Session, and an Item object is then created and appended to the Order.items collection of that Order, the Item will be automatically cascaded into that same 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

Above, the bidirectional nature of Order.items and Item.order means that appending to Order.items also assigns to Item.order. At the same time, the save-update cascade allowed for the Item object to be added to the same Session which the parent Order was already associated.

However, if the operation above is performed in the reverse direction, where Item.order is assigned rather than appending directly to Order.item, the cascade operation into the Session will not take place automatically, even though the object assignments Order.items and Item.order will be in the same state as in the previous example:

>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False

In the above case, after the Item object is created and all the desired state is set upon it, it should then be added to the Session explicitly:

>>> session.add(i1)

In older versions of SQLAlchemy, the save-update cascade would occur bidirectionally in all cases. It was then made optional using an option known as cascade_backrefs. Finally, in SQLAlchemy 1.4 the old behavior was deprecated and the cascade_backrefs option was removed in SQLAlchemy 2.0. The rationale is that users generally do not find it intuitive that assigning to an attribute on an object, illustrated above as the assignment of i1.order = o1, would alter the persistence state of that object i1 such that it’s now pending within a Session, and there would frequently be subsequent issues where autoflush would prematurely flush the object and cause errors, in those cases where the given object was still being constructed and wasn’t in a ready state to be flushed. The option to select between uni-directional and bi-directional behvaiors was also removed, as this option created two slightly different ways of working, adding to the overall learning curve of the ORM as well as to the documentation and user support burden.

参见

cascade_backrefs behavior deprecated for removal in 2.0 - background on the change in behavior for “cascade backrefs”

删除

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 标记为删除,在执行 flush 操作之后, 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 的默认行为是将 address1address2user1 解关联, 即将它们的外键引用设为 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 操作。 deletedelete-orphan 的组合覆盖了 SQLAlchemy 决定是将外键列设置为 NULL,还是彻底删除该行的两种情况。

该功能默认完全独立于数据库中通过 FOREIGN KEY 约束所定义的 CASCADE 行为。 为了更高效地与数据库级别的配置集成,应使用 使用具有 ORM 关系的外键 ON DELETE 级联 中描述的附加指令。

警告

请注意 ORM 中的 “delete” 和 “delete-orphan” 行为 仅适用于 通过 Session.delete() 方法, 在 unit of work 流程中标记要删除的单个 ORM 实例。 它 不适用于 “批量”删除操作,这类操作是通过 delete() 构造函数执行的, 示例参见 使用自定义 WHERE 条件的 ORM UPDATE 和 DELETE。 更多信息参见 启用 ORM 的更新和删除的重要说明和注意事项

The delete cascade indicates that when a “parent” object is marked for deletion, its related “child” objects should also be marked for deletion. If for example we have a relationship User.addresses with delete cascade configured:

class User(Base):
    # ...

    addresses = relationship("Address", cascade="all, delete")

If using the above mapping, we have a User object and two related Address objects:

>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses

If we mark user1 for deletion, after the flush operation proceeds, address1 and address2 will also be deleted:

>>> sess.delete(user1)
>>> sess.commit()
DELETE FROM address WHERE address.id = ? ((1,), (2,)) DELETE FROM user WHERE user.id = ? (1,) COMMIT

Alternatively, if our User.addresses relationship does not have delete cascade, SQLAlchemy’s default behavior is to instead de-associate address1 and address2 from user1 by setting their foreign key reference to NULL. Using a mapping as follows:

class User(Base):
    # ...

    addresses = relationship("Address")

Upon deletion of a parent User object, the rows in address are not deleted, but are instead de-associated:

>>> 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

删除 cascade on one-to-many relationships is often combined with 删除-孤立 cascade, which will emit a DELETE for the related row if the “child” object is deassociated from the parent. The combination of delete and delete-orphan cascade covers both situations where SQLAlchemy has to decide between setting a foreign key column to NULL versus deleting the row entirely.

The feature by default works completely independently of database-configured FOREIGN KEY constraints that may themselves configure CASCADE behavior. In order to integrate more efficiently with this configuration, additional directives described at 使用具有 ORM 关系的外键 ON DELETE 级联 should be used.

警告

Note that the ORM’s “delete” and “delete-orphan” behavior applies only to the use of the Session.delete() method to mark individual ORM instances for deletion within the unit of work process. It does not apply to “bulk” deletes, which would be emitted using the delete() construct as illustrated at 使用自定义 WHERE 条件的 ORM UPDATE 和 DELETE. See 启用 ORM 的更新和删除的重要说明和注意事项 for additional background.

使用具有多对多关系的删除级联

Using delete cascade with many-to-many relationships

cascade="all, delete" 选项同样适用于多对多关系, 该关系使用 relationship.secondary 参数指示一个关联表。 当父对象被删除并因此与其相关对象解除关联时,unit of work 过程通常会从关联表中删除对应的行, 但不会删除关联的子对象本身。而当结合使用 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",
    )

在上例中,当一个 Parent 对象通过 Session.delete() 被标记为删除时, flush 过程会像往常一样从 association 表中删除对应行, 但根据级联规则,也会将所有关联的 Child 行一并删除。

警告

如果上述 cascade="all, delete" 设置同时配置在 两个 关系上, 那么级联操作会在所有 ParentChild 对象之间不断传递, 加载所有遇到的 childrenparents 集合,并删除所有关联的对象。 通常 不建议 将 “delete” 级联设置为双向。

从多对多表中删除行

使用具有多对多关系的外键 ON DELETE

The cascade="all, delete" option works equally well with a many-to-many relationship, one that uses relationship.secondary to indicate an association table. When a parent object is deleted, and therefore de-associated with its related objects, the unit of work process will normally delete rows from the association table, but leave the related objects intact. When combined with cascade="all, delete", additional DELETE statements will take place for the child rows themselves.

The following example adapts that of 多对多 to illustrate the cascade="all, delete" setting on one side of the association:

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",
    )

Above, when a Parent object is marked for deletion using Session.delete(), the flush process will as usual delete the associated rows from the association table, however per cascade rules it will also delete all related Child rows.

警告

If the above cascade="all, delete" setting were configured on both relationships, then the cascade action would continue cascading through all Parent and Child objects, loading each children and parents collection encountered and deleting everything that’s connected. It is typically not desirable for “delete” cascade to be configured bidirectionally.

从多对多表中删除行

使用具有多对多关系的外键 ON DELETE

使用具有 ORM 关系的外键 ON DELETE 级联

Using foreign key ON DELETE cascade with ORM relationships

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

要在 relationship() 中使用 ON DELETE 外键级联, 首先需要注意的是:仍然必须配置 relationship.cascade 设置来匹配期望的 “delete” 或 “set null” 行为 (即使用 delete cascade 或保持默认), 这样无论由 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_parent 是一个 Parent 实例。

  2. Session 下一次 flush 数据库时,ORM 会删除 当前已加载my_parent.children 集合中的所有项,即对每个记录发出 DELETE 语句。

  3. 如果 my_parent.children 集合是 未加载 的,则不会发出任何 DELETE 语句。 如果这个 relationship() 没有设置 relationship.passive_deletes,则 ORM 会为未加载的 Child 对象发出 SELECT 查询。

  4. 接着会对 my_parent 所对应的父行发出 DELETE 语句。

  5. 数据库层级的 ON DELETE CASCADE 设置确保所有在 child 表中引用该 parent 行的行也会被删除。

  6. my_parent 所引用的 Parent 实例,以及所有与其相关联并已 加载Child 实例(即第 2 步中的那些),都会从 Session 中解除关联。

备注

要使用 “ON DELETE CASCADE”,底层数据库引擎必须支持并启用 FOREIGN KEY 约束:

The behavior of SQLAlchemy’s “delete” cascade overlaps with the ON DELETE feature of a database FOREIGN KEY constraint. SQLAlchemy allows configuration of these schema-level DDL behaviors using the ForeignKey and ForeignKeyConstraint constructs; usage of these objects in conjunction with Table metadata is described at ON UPDATE 和 ON DELETE.

In order to use ON DELETE foreign key cascades in conjunction with relationship(), it’s important to note first and foremost that the relationship.cascade setting must still be configured to match the desired “delete” or “set null” behavior (using delete cascade or leaving it omitted), so that whether the ORM or the database level constraints will handle the task of actually modifying the data in the database, the ORM will still be able to appropriately track the state of locally present objects that may be affected.

There is then an additional option on relationship() which indicates the degree to which the ORM should try to run DELETE/UPDATE operations on related rows itself, vs. how much it should rely upon expecting the database-side FOREIGN KEY constraint cascade to handle the task; this is the relationship.passive_deletes parameter and it accepts options False (the default), True and "all".

The most typical example is that where child rows are to be deleted when parent rows are deleted, and that ON DELETE CASCADE is configured on the relevant FOREIGN KEY constraint as well:

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")

The behavior of the above configuration when a parent row is deleted is as follows:

  1. The application calls session.delete(my_parent), where my_parent is an instance of Parent.

  2. When the Session next flushes changes to the database, all of the currently loaded items within the my_parent.children collection are deleted by the ORM, meaning a DELETE statement is emitted for each record.

  3. If the my_parent.children collection is unloaded, then no DELETE statements are emitted. If the relationship.passive_deletes flag were not set on this relationship(), then a SELECT statement for unloaded Child objects would have been emitted.

  4. A DELETE statement is then emitted for the my_parent row itself.

  5. The database-level ON DELETE CASCADE setting ensures that all rows in child which refer to the affected row in parent are also deleted.

  6. The Parent instance referred to by my_parent, as well as all instances of Child that were related to this object and were loaded (i.e. step 2 above took place), are de-associated from the Session.

备注

To use “ON DELETE CASCADE”, the underlying database engine must support FOREIGN KEY constraints and they must be enforcing:

使用具有多对多关系的外键 ON DELETE

Using foreign key ON DELETE with many-to-many relationships

使用具有多对多关系的删除级联 中所述,”delete” 级联同样适用于多对多关系。 要在多对多关系中结合使用 ON DELETE CASCADE 外键, 需要在关联表(association table)上配置 FOREIGN KEY 指令。 这些指令可以自动删除关联表中的数据,但 无法自动删除实际的关联对象本身

在这种情况下,relationship.passive_deletes 指令可以帮我们节省一部分删除操作过程中的 SELECT 语句, 但 ORM 仍然会加载部分集合,以便正确定位受影响的子对象并进行处理。

备注

一个设想中的优化方案是:对关联表中所有与父对象关联的行一次性发出 DELETE 语句, 然后使用 RETURNING 来获取受影响的子对象,但目前这并不是 ORM 工作单元(unit of work)实现的一部分。

在此配置中,我们在关联表中的两个外键约束上都配置了 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. 当执行 flush 时,如果 Parent.children 集合尚未加载,ORM 会先发出一条 SELECT 查询,以加载对应的 Child 对象。

  3. 随后 ORM 会发出 DELETE 语句,删除关联表 association 中对应该父对象的所有行。

4. 对于所有在这次删除中被影响的 Child 对象,由于配置了 passive_deletes=True, 工作单元将 不会 对每个 Child.parents 集合发出 SELECT 查询, 因为已假设关联表中的相关行会被删除。

  1. 最后,ORM 会对从 Parent.children 加载的每个 Child 对象发出 DELETE 语句。

As described at 使用具有多对多关系的删除级联, “delete” cascade works for many-to-many relationships as well. To make use of ON DELETE CASCADE foreign keys in conjunction with many to many, FOREIGN KEY directives are configured on the association table. These directives can handle the task of automatically deleting from the association table, but cannot accommodate the automatic deletion of the related objects themselves.

In this case, the relationship.passive_deletes directive can save us some additional SELECT statements during a delete operation but there are still some collections that the ORM will continue to load, in order to locate affected child objects and handle them correctly.

备注

Hypothetical optimizations to this could include a single DELETE statement against all parent-associated rows of the association table at once, then use RETURNING to locate affected related child rows, however this is not currently part of the ORM unit of work implementation.

In this configuration, we configure ON DELETE CASCADE on both foreign key constraints of the association table. We configure cascade="all, delete" on the parent->child side of the relationship, and we can then configure passive_deletes=True on the other side of the bidirectional relationship as illustrated below:

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,
    )

Using the above configuration, the deletion of a Parent object proceeds as follows:

  1. A Parent object is marked for deletion using Session.delete().

  2. When the flush occurs, if the Parent.children collection is not loaded, the ORM will first emit a SELECT statement in order to load the Child objects that correspond to Parent.children.

  3. It will then then emit DELETE statements for the rows in association which correspond to that parent row.

  4. for each Child object affected by this immediate deletion, because passive_deletes=True is configured, the unit of work will not need to try to emit SELECT statements for each Child.parents collection as it is assumed the corresponding rows in association will be deleted.

  5. DELETE statements are then emitted for each Child object that was loaded from Parent.children.

删除-孤立

delete-orphan

delete-orphan 级联是在 delete 级联基础上的增强, 使得子对象在 与父对象解除关联时 也会被标记为待删除,而不仅仅是在父对象被标记删除时。 当子对象“属于”其父对象,并且外键为 NOT NULL 时,这是一个常见用法, 这样当某个子对象从父对象集合中移除时,它就会被删除。

delete-orphan 级联隐含了 每个子对象只能同时拥有一个父对象。 在绝大多数情况下,它 仅适用于一对多关系。 对于较少见的将其用于多对一或多对多关系的情况, 可以通过配置 relationship.single_parent 参数来强制“多”的一方在任一时间只能关联一个对象, 此参数在 Python 层提供校验,确保对象只与一个父对象关联。 但这会 大幅限制 多重关系的功能,通常并非我们所期望的行为。

delete-orphan cascade adds behavior to the delete cascade, such that a child object will be marked for deletion when it is de-associated from the parent, not just when the parent is marked for deletion. This is a common feature when dealing with a related object that is “owned” by its parent, with a NOT NULL foreign key, so that removal of the item from the parent collection results in its deletion.

delete-orphan cascade implies that each child object can only have one parent at a time, and in the vast majority of cases is configured only on a one-to-many relationship. For the much less common case of setting it on a many-to-one or many-to-many relationship, the “many” side can be forced to allow only a single object at a time by configuring the relationship.single_parent argument, which establishes Python-side validation that ensures the object is associated with only one parent at a time, however this greatly limits the functionality of the “many” relationship and is usually not what’s desired.

合并

merge

merge 级联表示,当对一个父对象调用 Session.merge() 操作时,该操作会向其引用的对象传播。 该级联默认是开启的。

merge cascade indicates that the Session.merge() operation should be propagated from a parent that’s the subject of the Session.merge() call down to referred objects. This cascade is also on by default.

刷新-过期

refresh-expire

refresh-expire 是一个不常用的选项,表示 Session.expire() 操作应当从父对象向其引用对象传播。 当使用 Session.refresh() 时,引用对象仅会被标记为过期(expired),但不会真正刷新(refreshed)。

refresh-expire is an uncommon option, indicating that the Session.expire() operation should be propagated from a parent down to referred objects. When using Session.refresh(), the referred objects are expired only, but not actually refreshed.

删除

expunge

expunge 级联表示,当父对象通过 Session.expunge()Session 中移除时, 该操作也会向其引用对象传播。

expunge cascade indicates that when the parent object is removed from the Session using Session.expunge(), the operation should be propagated down to referred objects.

删除注意事项 - 删除从集合和标量关系中引用的对象

Notes on Delete - Deleting Objects Referenced from Collections and Scalar Relationships

一般来说,在 flush 过程中,ORM 不会修改集合或标量关系的内容。 也就是说,如果某个类有一个 relationship() 属性,指向一组对象或某个单个对象(如多对一关系), 在 flush 发生时,该属性的内容 不会被修改。 相反,我们预期这个 Session 最终会过期(expire), 要么通过 Session.commit() 的自动过期行为,要么通过显式调用 Session.expire()。 此时,该 Session 中引用的任何对象或集合都会被清空,并在下次访问时重新加载。

这种行为常常引发一个误解,特别是在使用 Session.delete() 方法时。 当我们对一个对象调用 Session.delete() 并 flush 时,对应的数据库行会被删除。 如果有其他行通过外键引用了这个被删除的行,且这些行是通过 relationship() 跟踪的, 那么这些引用行的外键会被设置为 null, 或者如果设置了删除级联(delete cascade),那么这些相关行也会被删除。 但即便如此, 在 flush 范围内,对象间的关系属性或集合在 Python 端仍保持原样。 也就是说,如果被删除的对象属于某个集合,它仍会存在于 Python 的该集合中,直到该集合被标记为过期。 同样,如果该对象是通过多对一或一对一的关系被另一个对象引用的,该引用也会继续存在,直到这个对象过期。

下面的示例演示了,尽管 Address 对象已被标记为删除并且 flush 了, 它仍然存在于父对象 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 级联会使该对象被标记为删除,效果等同于将其传递给 Session.delete()

delete-orphan 级联同样可以用于多对一或一对一关系。 这时,当一个对象从其父对象中解除关联时,它也会自动被标记为删除。 但在多对一或一对一中使用 delete-orphan 时,需要额外配置一个标志: relationship.single_parent, 该参数会在 Python 层面断言该对象 不能同时被多个父对象引用:

class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete-orphan", single_parent=True
    )

上面这个例子中,如果某个 Preference 对象从其所属 User 中移除,在 flush 时它就会被删除:

some_user.preference = None
session.flush()  # 将删除该 Preference 对象

参见

级联 获取有关级联行为的更多细节。

The ORM in general never modifies the contents of a collection or scalar relationship during the flush process. This means, if your class has a relationship() that refers to a collection of objects, or a reference to a single object such as many-to-one, the contents of this attribute will not be modified when the flush process occurs. Instead, it is expected that the Session would eventually be expired, either through the expire-on-commit behavior of Session.commit() or through explicit use of Session.expire(). At that point, any referenced object or collection associated with that Session will be cleared and will re-load itself upon next access.

A common confusion that arises regarding this behavior involves the use of the Session.delete() method. When Session.delete() is invoked upon an object and the Session is flushed, the row is deleted from the database. Rows that refer to the target row via foreign key, assuming they are tracked using a relationship() between the two mapped object types, will also see their foreign key attributes UPDATED to null, or if delete cascade is set up, the related rows will be deleted as well. However, even though rows related to the deleted object might be themselves modified as well, no changes occur to relationship-bound collections or object references on the objects involved in the operation within the scope of the flush itself. This means if the object was a member of a related collection, it will still be present on the Python side until that collection is expired. Similarly, if the object were referenced via many-to-one or one-to-one from another object, that reference will remain present on that object until the object is expired as well.

Below, we illustrate that after an Address object is marked for deletion, it’s still present in the collection associated with the parent User, even after a flush:

>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

When the above session is committed, all attributes are expired. The next access of user.addresses will re-load the collection, revealing the desired state:

>>> session.commit()
>>> address in user.addresses
False

There is a recipe for intercepting Session.delete() and invoking this expiration automatically; see ExpireRelationshipOnFKChange for this. However, the usual practice of deleting items within collections is to forego the usage of Session.delete() directly, and instead use cascade behavior to automatically invoke the deletion as a result of removing the object from the parent collection. The delete-orphan cascade accomplishes this, as illustrated in the example below:

class User(Base):
    __tablename__ = "user"

    # ...

    addresses = relationship("Address", cascade="all, delete-orphan")


# ...

del user.addresses[1]
session.flush()

Where above, upon removing the Address object from the User.addresses collection, the delete-orphan cascade has the effect of marking the Address object for deletion in the same way as passing it to Session.delete().

The delete-orphan cascade can also be applied to a many-to-one or one-to-one relationship, so that when an object is de-associated from its parent, it is also automatically marked for deletion. Using delete-orphan cascade on a many-to-one or one-to-one requires an additional flag relationship.single_parent which invokes an assertion that this related object is not to shared with any other parent simultaneously:

class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete-orphan", single_parent=True
    )

Above, if a hypothetical Preference object is removed from a User, it will be deleted on flush:

some_user.preference = None
session.flush()  # will delete the Preference object

参见

级联 for detail on cascades.