级联

映射器支持在 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() 函数一起使用,该函数最终将其参数反馈到 relationship()

class Item(Base):
    __tablename__ = "item"

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

relationship.cascade 的默认值为 save-update, merge。 此参数的典型替代设置是 all 或更常见的 all, delete-orphanall 符号是 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,它有两个关联的对象 address1address2

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

如果我们添加 user1Session,它也会隐式地添加 address1address2

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

save-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
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。 同时,save-update 级联允许将 Item 对象添加到父 Order 已经关联的同一 Session 中。

但是,如果上述操作以 相反 的方向执行,其中分配了 Item.order 而不是直接附加到 Order.item,则到 Session 的级联操作将 不会 自动发生,即使对象分配 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 中,旧的行为已被弃用,并且 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 的默认行为是将 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 级联通常与 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 启用更新和删除的重要注意事项和警告

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

将外键 ON DELETE 级联与 ORM 关系一起使用

SQLAlchemy 的“delete”级联的行为与数据库 FOREIGN KEY 约束的 ON DELETE 功能重叠。 SQLAlchemy 允许使用 ForeignKeyForeignKeyConstraint 构造配置这些模式级 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")

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

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

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

  3. 如果 my_parent.children 集合是 未加载的,则不会发出 DELETE 语句。 如果 relationship.passive_deletes 标志 在此 relationship() 上设置,则将为未加载的 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 与多对多关系一起使用

将删除级联与多对多关系一起使用 中所述,“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.children 对应的 Child 对象。

  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() 的 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

另请参阅

级联 有关级联的详细信息。