特殊关系持续模式

优质
小牛编辑
133浏览
2023-12-01

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

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

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

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

例如::

          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等序列的数据库可以使用以前生成的值立即插入行,但依赖自动增量样式主键标识符的数据库则不能。这个 relationship() 在刷新期间始终假定行填充的“父/子”模型,因此除非直接填充主键/外键列, relationship() 需要使用两个语句。

在第二种情况下,必须在任何引用的“entry”行之前插入“widget”行,但在生成“entry”行之前,不能设置“widget”行的“favorite”列。在这种情况下,通常不可能只使用两条INSERT语句插入“widget”和“entry”行;必须执行更新才能满足外键约束。例外情况是,如果将外键配置为“延迟到提交”(某些数据库支持的功能),以及手动填充标识符(再次基本上绕过 relationship()

为了能够使用补充更新语句,我们使用 relationship.post_update 选择权 relationship() . 这指定在插入两行后,应使用UPDATE语句创建两行之间的链接;它还将导致在发出删除之前通过UPDATE取消行之间的关联。旗子应该放在 one 最好是多对一的关系。下面我们将展示一个完整的示例,其中包括两个 ForeignKey 结构:

from sqlalchemy import Integer, ForeignKey, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

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

class Widget(Base):
    __tablename__ = 'widget'

    widget_id = Column(Integer, primary_key=True)
    favorite_entry_id = Column(Integer,
                            ForeignKey('entry.entry_id',
                            name="fk_favorite_entry"))
    name = 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”行将被插入减去“favorite”(最爱)条目“id”值,然后所有“entry”(条目)行将被插入引用父“widget”(小部件)行,然后更新语句将填充“widget”(小部件)表的“favorite”(最爱)条目“id”(每次一行)列(此时g):

>>> w1 = Widget(name='somewidget')
>>> e1 = Entry(name='someentry')
>>> w1.favorite_entry = e1
>>> w1.entries = [e1]
>>> session.add_all([w1, e1])
sql>>> 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, \
        Column, UniqueConstraint, ForeignKeyConstraint
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

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

class Widget(Base):
    __tablename__ = 'widget'

    widget_id = Column(Integer, autoincrement='ignore_fk', primary_key=True)
    favorite_entry_id = Column(Integer)

    name = 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 = Column(String(50), primary_key=True)
    fullname = Column(String(100))

    addresses = relationship("Address")


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

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

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

参见

在具有ORM关系的DELETE cascade中使用外键 -支持删除带关系的级联

mapper.passive_updates - similar feature on mapper()

在没有外键支持的情况下模拟有限更新级联

在这些情况下,如果使用了不支持引用完整性的数据库,并且使用了具有可变值的自然主键,则SQLAlchemy提供了一个特性,以便允许将主键值传播到已引用的外键到 有限的 扩展,方法是针对立即引用其值已更改的主键列的外键列发出UPDATE语句。没有引用完整性功能的主要平台是mysql,当 MyISAM 使用存储引擎,当 PRAGMA foreign_keys=ON 未使用pragma。Oracle数据库也不支持 ON UPDATE CASCADE 但由于它仍然强制引用完整性,需要将约束标记为可延迟,以便SQLAlchemy可以发出UPDATE语句。

通过设置 relationship.passive_updates 旗到 False ,最好是一对多或多对多 relationship() . 当“updates”不再是“passive”时,这表示SQLAlchemy将为父对象引用的集合中引用的对象单独发出update语句,该对象的主键值将发生更改。这还意味着,如果集合不在本地存在,那么它将被完全加载到内存中。

我们以前的映射使用 passive_updates=False 看起来像:

class User(Base):
    __tablename__ = 'user'

    username = Column(String(50), primary_key=True)
    fullname = 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 = Column(String(50), primary_key=True)
    username = Column(String(50), ForeignKey('user.username'))

关键限制 passive_updates=False 包括:

  • 它在update cascade上的性能比direct数据库差得多,因为它需要使用select完全预加载受影响的集合,还必须针对这些值发出update语句,它将尝试在“批处理”中运行,但仍在dbapi级别按行运行。

  • 该功能不能“层叠”多个级别。也就是说,如果映射X有一个外键引用映射Y的主键,那么映射Y的主键本身就是映射Z的外键, passive_updates=False 无法层叠主键值的更改 ZX .

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

实际上除了Oracle以外的所有数据库都支持 ON UPDATE CASCADE ,强烈建议 ON UPDATE CASCADE 支持用于使用自然和可变主键值的情况。