特殊关系持久化模式¶
指向自身的行 / 相互依赖的行¶
这是一个非常特殊的情况,其中 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 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
支持在自然和可变主键值位于
用。