特殊关系持久化模式

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

这是一个非常特殊的情况,其中 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() 始终假设在 flush 期间行填充的“父/子”模型,因此除非您直接填充主键/外键列,否则 relationship() 需要使用两个语句。

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

为了能够使用补充的 UPDATE 语句,我们使用了 relationship.post_updaterelationship() 选项。这指定了应该使用 UPDATE 语句在两行都 INSERT 后创建两行之间的链接;它还会在发出 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
    )

当对上述配置的结构进行 flush 时,将 INSERT “widget”行,但没有 “favorite_entry_id” 值,然后将 INSERT 所有引用父 “widget” 行的 “entry” 行,最后执行 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.autoincrement 上指定值 "ignore_fk"Column,此外,在每个 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 级联与 ORM 关系 - 使用关系支持 ON DELETE 级联

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 无法级联从 ZX 的主键值的更改。

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

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