特殊关系持久化模式


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


这是一个非常特殊的情况,其中 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 Database 等序列可以一次 INSERT 该行 使用以前生成的值,但依赖于 autoincrement 样式的主键标识符不能。这 relationship() 在 flush 期间始终假定行填充的“父/子”模型,因此除非你直接填充主键/外键列,否则 relationship() 需要使用两个语句。


在第二种情况下,必须在任何引用之前插入 “widget” 行 “entry” 行,但随后是该 “widget” 行的 “favorite_entry_id” 列 在生成 “entry” 行之前无法设置。在本例中,它是 通常不可能只使用两个行来插入 “widget” 和 “entry” 行 INSERT 语句;必须执行 UPDATE 才能保留外键 constraints fulfilled 的 Constraints。例外情况是,如果外键配置为 “deferred until commit”(某些数据库支持此功能),并且如果 标识符是手动填充的(同样基本上绕过了 relationship())的


为了启用补充 UPDATE 语句的使用,我们使用 relationship()relationship.post_update 选项。这指定在两行都已插入后,应使用 UPDATE 语句创建两行之间的链接;它还会导致在发出 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”行将被引用父“widget”行的 INSERT,然后 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 引用一个 Entry ,也指此 Widget。我们可以使用复合外键,如下图所示:

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_id 列和 favorite_entry_id 列。为了确保 Widget.widget_id 仍然是 “自动递增” 列,我们指定 Column.autoincrement 设置为值 “ignore_fk”Column 上,另外在每个 relationship() 时,必须限制那些被视为外键一部分的列,以便进行 join 和 cross-population。


可变主键 / 更新级联


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


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

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” object,我们还演示了 mysql_engine='InnoDB' 设置,该设置在 MySQL 后端确保 InnoDB 引擎支持 使用引用完整性。 使用 SQLite 时,引用完整性 应启用,请使用 外键支持


另请参阅


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


mapper.passive_updates - Mapper 上的类似功能


模拟无外键支持的有限 ON UPDATE CASCADE


在那些情况下,当使用不支持引用完整性的数据库,并且具有可变值的自然主键正在发挥作用时,SQLAlchemy提供了一项功能,以便允许将主键值在有限范围内传播到已经引用的外键,方法是针对立即引用其值已更改的主键列的外键列发出UPDATE语句。没有引用完整性功能的主要平台是 MySQL(当使用 MyISAM 存储引擎时)和 SQLite(当 PRAGMA foreign_keys=ON 未使用 PRAGMA。Oracle Database 也不支持 ON UPDATE CASCADE,但由于它仍然强制执行引用完整性,因此需要将约束标记为可延迟,以便 SQLAlchemy 可以发出 UPDATE 语句。


该功能可通过设置 relationship.passive_updates标志设置为 False,最好是在一对多或多对多 relationship() 上。当 “updates” 不再是 “被动” 时,这表明 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 的 CASCADE 中,强烈建议使用传统的 ON UPDATE CASCADE 支持在自然和可变主键值位于 用。