特殊关系持久化模式

指向自身的行 / 相互依赖的行

这是一个非常特殊的情况,其中 relationship() 必须执行 INSERT 和第二次 UPDATE 才能正确填充行(反之亦然,执行 UPDATE 和 DELETE 才能在不违反外键约束的情况下删除)。 这两种用例是:

  • 一个表包含指向自身的外键,并且单行将具有指向自身主键的外键值。

  • 两个表各自包含一个外键,引用另一个表,并且每个表中的一行引用另一个表。

例如

          user
---------------------------------
user_id    name   related_user_id
   1       'ed'          1

             widget                                                  entry
-------------------------------------------             ---------------------------------
widget_id     name        favorite_entry_id             entry_id      name      widget_id
   1       'somewidget'          5                         5       'someentry'     1

在第一种情况下,一行指向自身。 从技术上讲,使用序列的数据库(如 PostgreSQL 或 Oracle 数据库)可以使用先前生成的值一次性 INSERT 该行,但依赖于自增式主键标识符的数据库则不能。 relationship() 始终假定刷新期间行的填充采用“父/子”模型,因此除非您直接填充主键/外键列,否则 relationship() 需要使用两个语句。

在第二种情况下,“widget”行必须在任何引用的“entry”行之前插入,但是只有在生成“entry”行之后才能设置该“widget”行的“favorite_entry_id”列。 在这种情况下,通常不可能仅使用两个 INSERT 语句来插入“widget”和“entry”行; 必须执行 UPDATE 才能保持外键约束得到满足。 除非外键配置为“延迟到提交”(某些数据库支持的功能),并且标识符是手动填充的(再次基本上绕过 relationship()),否则会出现例外。

为了启用补充 UPDATE 语句的使用,我们使用 relationship()relationship.post_update 选项。 这指定两个行之间的链接应在使用 UPDATE 语句在两个行都已 INSERTED 之后创建; 它还会导致行在发出 DELETE 之前通过 UPDATE 彼此取消关联。 该标志应仅放置在一个关系上,最好是多对一侧。 下面我们说明一个完整的示例,包括两个 ForeignKey 构造

from sqlalchemy import Integer, ForeignKey
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class Entry(Base):
    __tablename__ = "entry"
    entry_id = mapped_column(Integer, primary_key=True)
    widget_id = mapped_column(Integer, ForeignKey("widget.widget_id"))
    name = mapped_column(String(50))


class Widget(Base):
    __tablename__ = "widget"

    widget_id = mapped_column(Integer, primary_key=True)
    favorite_entry_id = mapped_column(
        Integer, ForeignKey("entry.entry_id", name="fk_favorite_entry")
    )
    name = mapped_column(String(50))

    entries = relationship(Entry, primaryjoin=widget_id == Entry.widget_id)
    favorite_entry = relationship(
        Entry, primaryjoin=favorite_entry_id == Entry.entry_id, post_update=True
    )

当刷新针对上述配置的结构时,“widget”行将被 INSERT,但不包括“favorite_entry_id”值,然后所有“entry”行将被 INSERT,引用父“widget”行,然后 UPDATE 语句将填充“widget”表的“favorite_entry_id”列(目前一次一行)

>>> w1 = Widget(name="somewidget")
>>> e1 = Entry(name="someentry")
>>> w1.favorite_entry = e1
>>> w1.entries = [e1]
>>> session.add_all([w1, e1])
>>> session.commit()
BEGIN (implicit) INSERT INTO widget (favorite_entry_id, name) VALUES (?, ?) (None, 'somewidget') INSERT INTO entry (widget_id, name) VALUES (?, ?) (1, 'someentry') UPDATE widget SET favorite_entry_id=? WHERE widget.widget_id = ? (1, 1) COMMIT

我们可以指定的另一个配置是在 Widget 上提供更全面的外键约束,以确保 favorite_entry_id 引用也引用此 WidgetEntry。 我们可以使用复合外键,如下所示

from sqlalchemy import (
    Integer,
    ForeignKey,
    String,
    UniqueConstraint,
    ForeignKeyConstraint,
)
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class Entry(Base):
    __tablename__ = "entry"
    entry_id = mapped_column(Integer, primary_key=True)
    widget_id = mapped_column(Integer, ForeignKey("widget.widget_id"))
    name = mapped_column(String(50))
    __table_args__ = (UniqueConstraint("entry_id", "widget_id"),)


class Widget(Base):
    __tablename__ = "widget"

    widget_id = mapped_column(Integer, autoincrement="ignore_fk", primary_key=True)
    favorite_entry_id = mapped_column(Integer)

    name = mapped_column(String(50))

    __table_args__ = (
        ForeignKeyConstraint(
            ["widget_id", "favorite_entry_id"],
            ["entry.widget_id", "entry.entry_id"],
            name="fk_favorite_entry",
        ),
    )

    entries = relationship(
        Entry, primaryjoin=widget_id == Entry.widget_id, foreign_keys=Entry.widget_id
    )
    favorite_entry = relationship(
        Entry,
        primaryjoin=favorite_entry_id == Entry.entry_id,
        foreign_keys=favorite_entry_id,
        post_update=True,
    )

上述映射具有一个复合 ForeignKeyConstraint,桥接 widget_idfavorite_entry_id 列。 为了确保 Widget.widget_id 仍然是一个“自增”列,我们在 Column 上将 Column.autoincrement 指定为值 "ignore_fk",并且在每个 relationship() 上,我们都必须限制那些被认为是外键一部分的列,以便进行连接和交叉填充。

可变主键 / 更新级联

当实体的主键更改时,引用该主键的相关项也必须更新。 对于强制引用完整性的数据库,最佳策略是使用数据库的 ON UPDATE CASCADE 功能,以便将主键更改传播到引用的外键 - 除非约束标记为“可延迟”,即在事务完成之前不强制执行,否则这些值在任何时候都不能不同步。

强烈建议寻求使用具有可变值的自然主键的应用程序使用数据库的 ON UPDATE CASCADE 功能。 以下是一个说明此点的示例映射:

class User(Base):
    __tablename__ = "user"
    __table_args__ = {"mysql_engine": "InnoDB"}

    username = mapped_column(String(50), primary_key=True)
    fullname = mapped_column(String(100))

    addresses = relationship("Address")


class Address(Base):
    __tablename__ = "address"
    __table_args__ = {"mysql_engine": "InnoDB"}

    email = mapped_column(String(50), primary_key=True)
    username = mapped_column(
        String(50), ForeignKey("user.username", onupdate="cascade")
    )

上面,我们在 ForeignKey 对象上说明了 onupdate="cascade",并且我们还说明了 mysql_engine='InnoDB' 设置,该设置在 MySQL 后端上确保使用支持引用完整性的 InnoDB 引擎。 使用 SQLite 时,应启用引用完整性,使用 外键支持 中描述的配置。

另请参阅

将外键 ON DELETE cascade 与 ORM 关系一起使用 - 支持与关系的 ON DELETE CASCADE

mapper.passive_updates - Mapper 上的类似功能

在没有外键支持的情况下模拟有限的 ON UPDATE CASCADE

在那些使用不支持引用完整性的数据库,并且使用具有可变值的自然主键的情况下,SQLAlchemy 提供了一个功能,以便通过针对立即引用值已更改的主键列的外键列发出 UPDATE 语句,从而允许将主键值传播到已引用的外键,但程度有限。 不支持引用完整性功能的主要平台是使用 MyISAM 存储引擎时的 MySQL,以及未使用 PRAGMA foreign_keys=ON 编译指示时的 SQLite。 Oracle 数据库也不支持 ON UPDATE CASCADE,但由于它仍然强制执行引用完整性,因此需要将约束标记为可延迟,以便 SQLAlchemy 可以发出 UPDATE 语句。

通过将 relationship.passive_updates 标志设置为 False 来启用该功能,最好是在一对多或多对多 relationship() 上。 当“更新”不再是“被动”时,这表示 SQLAlchemy 将为父对象(主键值正在更改)引用的集合中引用的对象单独发出 UPDATE 语句。 这也意味着,如果集合尚未本地存在,则会将其完全加载到内存中。

我们之前使用 passive_updates=False 的映射如下所示

class User(Base):
    __tablename__ = "user"

    username = mapped_column(String(50), primary_key=True)
    fullname = mapped_column(String(100))

    # passive_updates=False *only* needed if the database
    # does not implement ON UPDATE CASCADE
    addresses = relationship("Address", passive_updates=False)


class Address(Base):
    __tablename__ = "address"

    email = mapped_column(String(50), primary_key=True)
    username = mapped_column(String(50), ForeignKey("user.username"))

passive_updates=False 的主要限制包括

  • 它的性能比直接数据库 ON UPDATE CASCADE 差得多,因为它需要使用 SELECT 完全预加载受影响的集合,并且还必须针对这些值发出 UPDATE 语句,它会尝试以“批处理”方式运行这些语句,但在 DBAPI 级别仍然按行运行。

  • 该功能不能“级联”超过一个级别。 也就是说,如果映射 X 具有引用映射 Y 的主键的外键,但是映射 Y 的主键本身是映射 Z 的外键,则 passive_updates=False 无法将主键值的更改从 Z 级联到 X

  • 仅在关系的多对一侧配置 passive_updates=False 不会产生完全效果,因为工作单元仅在当前标识映射中搜索可能引用具有可变主键的对象的对象,而不是在整个数据库中搜索。

由于除 Oracle 数据库之外的几乎所有数据库现在都支持 ON UPDATE CASCADE,因此强烈建议在使用自然主键和可变主键值的情况下使用传统的 ON UPDATE CASCADE 支持。