SQLAlchemy 2.0 级联(Cascades)部分翻译

官方文档:https://docs.sqlalchemy.org/en/20/orm/cascades.html#cascades

FastAPI管理后台框架:https://gitee.com/ktianc/kinit

级联(Cascades)

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

级联的默认行为仅限于所谓的保存-更新(save-update)和合并(merge)设置的级联。级联的典型“替代”设置是添加删除(delete)和删除孤儿(delete-orphan)选项;这些设置适用于只要附加到其父对象,相关对象就存在,并且在其他情况下将被删除。

可以使用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")
    )

级联的起源

SQLAlchemy对于关系(relationship)的级联行为以及配置级联的选项主要来自于Hibernate ORM中类似的功能。Hibernate在一些地方引用“cascade”,例如在“示例:父/子”中。如果级联行为令人困惑,我们会引用其中的结论,指出“我们刚刚涵盖的部分可能有点令人困惑。然而,在实践中,一切都很好地解决了。”

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)扩展时,这可能不是理想的,因为它会比在显式I/O上下文中通常适用的更积极地使相关对象过期。有关详细信息,请参阅在使用AsyncSession时阻止隐式I/O的背景说明。

可以为relationship.cascade参数指定的可用值列表在以下子节中进行描述。

save-update

save-update级联表示当通过Session.add()将对象放入会话(Session)中时,通过此relationship()关系与其相关的所有对象也应该添加到同一个会话中。假设我们有一个对象user1和两个相关对象address1和address2:

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

如果我们将user1添加到会话中,它也会隐式地添加address1和address2:

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

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

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

当从集合中移除项或将对象从标量属性中解除关联时,save-update级联可能会表现出令人惊讶的行为。在某些情况下,孤立的对象可能仍然会被引入到前父对象的会话中;这是为了让刷新过程可以适当地处理相关对象。通常只有在将对象从一个会话中删除并添加到另一个会话中时才会出现这种情况:

>>> 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 in sess2  # 因为它仍然是“等待刷新”的状态
True

save-update级联默认为打开状态,通常被认为是理所当然的;它通过允许一个单独的Session.add()调用一次性在会话中注册整个对象结构来简化代码。虽然可以禁用它,但通常没有这样的需求。

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

在双向关系的上下文中,save-update级联行为单向进行,即在使用relationship.back_populates或relationship.backref参数创建相互引用的两个独立的relationship()对象时。

一个与会话(Session)无关联的对象,当分配给与会话关联的父对象的属性或集合时,会自动添加到相同的会话中。然而,反向的相同操作则不会产生此效果;一个没有与会话关联的对象,被分配给一个与会话关联的子对象后,不会自动将该父对象添加到会话中。这个行为的总体主题被称为“级联反向引用”,并且代表了从SQLAlchemy 2.0开始标准化的行为变更。

为了说明这一点,考虑一个映射Order对象的例子,这些对象通过关系Order.items和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")},
)

如果一个Order已经与一个会话相关联,并且创建一个Item对象并将其附加到该Order的Order.items集合中,那么该Item将自动级联到相同的会话中:

>>> 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已经关联的相同会话中。

然而,如果在相反的方向上执行上述操作,即分配给Item.order而不是直接附加到Order.items,那么级联操作不会自动发生,即使对象分配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.add(i1)

在旧版本的SQLAlchemy中,save-update级联在所有情况下都会双向发生。然后,通过一个名为cascade_backrefs的选项,它变成了可选的。最终,在SQLAlchemy 1.4中,旧的行为被弃用,并且在SQLAlchemy 2.0中删除了cascade_backrefs选项。这么做的理由是,通常用户不认为在对象的属性上进行分配会改变该对象的持久化状态,正如上面的i1.order = o1的分配,使得它现在在会话中挂起,并且通常会出现后续问题,其中自动刷新(autoflush)会过早地刷新对象并导致错误,这些情况下给定的对象仍在构造中并且尚未准备好刷新。选择在单向和双向行为之间选择的选项也被删除了,因为此选项创建了两种稍微不同的工作方式,增加了ORM的整体学习曲线,以及文档和用户支持负担。

另请参阅

已弃用2.0中将被删除的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标记为删除,在刷新操作执行后,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约束,这些约束本身可能配置了级联行为。为了更有效地与这个配置集成,应该使用在“使用ORM关系的外键ON DELETE级联”中描述的附加指令。

另请参阅

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

在多对多关系中使用delete级联

delete-orphan

在多对多关系中使用delete级联

在使用relationship.secondary指示关联表的多对多关系中,cascade="all, delete"选项同样适用。当删除父对象,因此与其相关的对象解除关联时,工作单元过程通常会从关联表中删除行,但保留相关对象。与cascade="all, delete"结合使用时,将会为子行本身执行额外的DELETE语句。

下面的示例是根据Many To Many进行调整的,以说明如何在关联的一侧上设置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对象进行删除时,刷新过程通常会从关联表中删除关联的行,但根据级联规则,它还将删除所有相关的Child行。

警告

如果在上述两个关系上都配置了cascade="all, delete"设置,那么级联操作将继续通过所有的Parent和Child对象进行级联,加载每个遇到的children和parents集合,并删除所有连接的内容。通常不希望双向配置“delete”级联。

另请参阅

从多对多表中删除行

在多对多关系中使用外键ON DELETE

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

SQLAlchemy的“delete”级联行为与数据库外键约束的ON DELETE特性重叠。SQLAlchemy允许使用ForeignKey和ForeignKeyConstraint构造配置这些模式级别的DDL行为;使用这些对象与Table元数据结合的用法在ON UPDATE和ON DELETE中进行了描述。

为了在关系中使用ON DELETE外键级联,首先需要注意relationship.cascade设置必须配置为与所需的“delete”或“set null”行为相匹配(使用delete cascade或留空),以便无论是ORM还是数据库级约束都可以处理实际修改数据库中数据的任务,ORM仍能够适当地跟踪可能受到影响的本地存在的对象的状态。

然后,在relationship()上还有一个附加选项,指示ORM应在多大程度上自行运行DELETE/UPDATE操作以处理相关行,而不是多大程度上依赖于期望数据库端的外键约束级联来处理任务;这是relationship.passive_deletes参数,它接受False(默认值),True和"all"等选项。

最典型的例子是在删除父行时删除子行,并且在相关的外键约束上配置了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下次刷新对数据库的更改时,所有在my_parent.children集合中当前加载的项目都会被ORM删除,这意味着会为每个记录发出DELETE语句。
  3. 如果my_parent.children集合未加载,则不会发出DELETE语句。如果在此relationship()上未设置relationship.passive_deletes标志,那么将会发出一个未加载的Child对象的SELECT语句。
  4. 然后为my_parent行本身发出DELETE语句。
  5. 数据库级别的ON DELETE CASCADE设置确保删除对父行中受影响的行的所有引用的行。
  6. my_parent引用的Parent实例,以及与此对象相关并且已加载的所有Child实例(即已执行步骤2),将从Session中解除关联。

请注意

要使用“ON DELETE CASCADE”,底层数据库引擎必须支持FOREIGN KEY约束,并且它们必须是强制性的:

  • 在使用MySQL时,必须选择适当的存储引擎。有关详细信息,请参阅包括存储引擎在内的CREATE TABLE参数。
  • 在使用SQLite时,必须显式启用外键支持。有关详细信息,请参阅外键支持。

关于被动删除的注意事项

重要的是要注意ORM和关系数据库的“级联”概念以及它们如何集成之间的区别:

  • 数据库级别的ON DELETE级联实际上是在关系的“多对一”方面进行配置的;也就是说,我们将其相对于是关系的“多”的一侧的FOREIGN KEY约束进行配置。在ORM级别,这个方向是相反的。SQLAlchemy根据“父”一侧删除“子”对象,这意味着在“一对多”一侧上配置了delete和delete-orphan级联。

  • 没有ON DELETE设置的数据库级外键常常用于阻止删除父行,因为这必然会导致未处理的相关行存在。如果在一对多关系中需要这种行为,可以通过以下两种方式之一捕获SQLAlchemy的默认行为,即将外键列设置为NOT NULL:

    • 最简单和最常见的方法是仅在数据库模式级别将外键列设置为NOT NULL。SQLAlchemy试图将列设置为NULL将会导致简单的NOT NULL约束异常。
    • 另一种更特殊的情况是将relationship.passive_deletes标志设置为字符串"all"。这将完全禁用SQLAlchemy将外键列设置为NULL的行为,而在没有对子行产生任何影响的情况下,将会发出父行的DELETE语句,而子行不受影响,即使子行在内存中存在。在所有情况下,无论ORM还是数据库级别的外键触发器是否需要在删除父行时激活,这可能在数据库级别上都是可取的。
  • 数据库级别的ON DELETE级联通常比依赖于SQLAlchemy的“级联”删除功能要更高效。数据库可以在一个单独的DELETE语句的范围内链接一系列级联操作;例如,如果删除了行A,则可以删除与表B中的所有相关行,以及与每个B行相关的所有C行,等等。然而,为了完全支持级联删除操作,SQLAlchemy必须逐个加载每个相关集合,以便定位那些可能具有进一步相关集合的所有行。也就是说,在此上下文中,SQLAlchemy不足以一次性发出所有这些相关行的DELETE。

  • SQLAlchemy不需要如此复杂,因为我们实际上提供了与数据库本身的ON DELETE功能的平滑集成,方法是在relationship.passive_deletes选项与正确配置的外键约束结合使用。在此行为下,SQLAlchemy仅为已经在Session中本地存在的那些行发出DELETE;对于任何未加载的集合,它将它们交给数据库处理,而不是发出SELECT。使用外键ON DELETE级联与ORM关系提供了此用法的示例。

  • 尽管数据库级别的ON DELETE功能仅在关系的“多”的一侧上起作用,但SQLAlchemy的“delete”级联也有在相反方向上操作的有限能力,也就是说,它可以在“多”的一侧上配置为在“一”的一侧上删除一个对象,当在“多”的一侧上的引用被删除时。然而,如果其他对象从“多”的一侧引用了此“一”的一侧,则这很容易导致约束违规,因此通常仅在关系实际上是“一对一”关系时才有用。在这种情况下,应该使用relationship.single_parent标志来建立此案例的Python断言。

使用外键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,
    )

使用上述配置,删除父对象的操作如下:

  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级联在删除级联上添加了行为,使得当子对象与父对象取消关联时,子对象将被标记为删除,而不仅仅是在父对象标记为删除时。这在处理与其父对象“拥有”关系的相关对象时很常见,该相关对象具有NOT NULL外键,因此从父集合中删除该项将导致其被删除。

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

另请参阅

对于关系,delete-orphan级联通常仅配置在一对多关系的“一”方,而不在多对一关系或多对多关系的“多”方。 - 关于涉及delete-orphan级联的常见错误场景的背景信息。

merge

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

refresh-expire

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

expunge

expunge cascade 表示当通过 Session.expunge() 从会话中移除父对象时,此操作应该向下传播到引用的对象。

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

ORM 通常在刷新过程中不会修改集合或标量关系的内容。这意味着,如果您的类具有引用对象集合或引用单个对象(如多对一关系)的 relationship(),在执行刷新过程时,该属性的内容不会被修改。相反,预期会话最终会过期,可以通过 Session.commit() 的 expire-on-commit 行为或通过显式使用 Session.expire() 来实现。此时,与该会话关联的任何引用对象或集合将被清除,并将在下一次访问时重新加载自身。

关于此行为常常引发混淆的一个常见情况涉及使用 Session.delete() 方法。当调用 Session.delete() 于一个对象上并且会话被刷新时,行将从数据库中删除。与目标行通过外键引用的行(假设它们是通过两个映射对象类型之间的 relationship() 跟踪的),也将看到其外键属性被更新为 null,或者如果设置了 delete cascade,则相关行也将被删除。然而,即使与删除的对象相关的行可能也被修改,刷新本身的范围内操作中,与操作涉及的对象上的关系绑定集合或对象引用不会发生任何更改。这意味着如果对象是相关集合的成员,它将仍然存在于 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 级联会以与传递给 Session.delete() 相同的方式将 Address 对象标记为删除。

delete-orphan 级联也可以应用于多对一或一对一关系,使得当对象从其父对象解除关联时,它也会自动标记为删除。在多对一或一对一上使用 delete-orphan 级联需要一个额外的标志 relationship.single_parent,该标志会调用断言,确保此相关对象不会与任何其他父对象同时共享:

class User(Base):
    # ...

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

在上面的示例中,如果从 User 中移除一个假设的 Preference 对象,它将在刷新时被删除:

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

另请参见

关于级联的详细信息。

概要总结

  1. 级联类型:级联操作是一种在父对象进行操作时,自动影响其关联子对象的行为。常见的级联类型包括 save-updatedeletemergerefresh-expireexpunge
  2. save-update 级联save-update 级联表示当将一个对象通过 Session.add() 方法添加到会话中时,与它相关联的所有对象也将被添加到相同的会话中。这也适用于已经在会话中的对象。
  3. delete 级联delete 级联表示当一个“父”对象被标记为删除时,其相关的“子”对象也应该被标记为删除。可以与 delete-orphan 结合使用,以在子对象与父对象解关联时触发删除操作。
  4. delete-orphan 级联delete-orphan 级联是 delete 级联的补充,表示当一个子对象与父对象解关联时,该子对象也应该被标记为删除。通常用于拥有“所有权”的子对象。
  5. merge 级联merge 级联表示 Session.merge() 操作应该从父对象传播到引用的对象。默认情况下也是开启的。
  6. refresh-expire 级联refresh-expire 级联是一种不常见的选项,表示 Session.expire() 操作应该从父对象传播到引用的对象。
  7. expunge 级联expunge 级联表示当从会话中移除父对象时,操作应该传播到引用的对象。
  8. 集合和标量关系的行为:ORM 不会在刷新过程中修改集合或标量关系的内容。当父对象被删除时,与之关联的行可能会被更新,但关联的集合或对象引用不会在刷新过程中发生变化。这意味着在刷新之前,相关的对象或集合仍然存在于 Python 中,直到会话过期。
  9. 使用 Session.delete() 的注意事项:当调用 Session.delete() 删除一个对象时,与之相关的外键引用也会被更新,甚至可能触发级联删除操作,但刷新过程不会直接修改集合或对象引用。
  10. 通过 delete-orphan 级联删除对象:使用 delete-orphan 级联可以自动将对象从父集合中删除,达到标记删除的效果,而不需要直接使用 Session.delete()
  11. 关于级联的推荐做法:在集合中删除项目的常见做法是使用级联行为,而不是直接使用 Session.delete()。对于多对一或一对一关系,可以在父对象解关联时使用 delete-orphan 级联,并在此基础上配置 relationship.single_parent

你可能感兴趣的:(sqlalchemy,2.0,python,sqlalchemy)