Special Relationship Persistence Patterns

Rows that point to themselves / Mutually Dependent Rows

これは、relationship()が行を適切に設定するためにINSERTと2番目のUPDATEを実行する(また、外部キー制約に違反せずに削除するためにUPDATEとDELETEを実行する)必要がある非常に特殊なケースです。次の2つのユースケースがあります。

  • テーブルにはそれ自体に対する外部キーが含まれ、1つのローにはそれ自体のプライマリ・キーを指す外部キー値が含まれます。

  • 2つのテーブルにはそれぞれ、もう一方のテーブルを参照する外部キーが含まれ、各テーブルのローはもう一方のテーブルを参照します。

次に例を示します。:

          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() は2つの文を使用する必要があります。

2番目のケースでは、”widget”行は参照する”entry”行の前に挿入されなければなりませんが、その”widget”行の”favorite_entry_id”列は”entry”行が生成されるまで設定できません。この場合、2つのINSERT文だけで”widget”行と”entry”行を挿入することは通常不可能です。外部キー制約を満たすためにUPDATEを実行する必要があります。例外は、外部キーが”deferred until commit”(一部のデータベースでサポートされている機能)として設定されている場合と、識別子が手動で入力された場合です(これも基本的に relationship() をバイパスしています)。

補足的なUPDATE文の使用を有効にするには、 relationship()relationship.post_update オプションを使用します。これは、2つの行の間のリンクが、両方の行がINSERTEDされた後にUPDATE文を使用して作成されることを指定します。また、DELETEが発行される前に、UPDATEによって行同士の関連付けが解除されます。フラグは、関係の*1*だけ、できれば多対1の側に配置する必要があります。以下に、2つの 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”行から”favorite_entry_id”値を引いたものが挿入され、親の”widget”行を参照してすべての”entry”行が挿入されてから、UPDATE文によって”widget”テーブルの”favorite_entry_id”列が生成されます(当面は一度に1行ずつ)。

>>> 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 がこの Widget も参照する Entry を参照することが保証されます。以下に示すように、複合外部キーを使用できます。:

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`が”自動インクリメント”列のままであることを保証するために、 :paramref:`_schema.Column.autoincrementColumn の値`”ignore_fk”に指定します。さらに、各 :func:`_orm.relationship では、結合とクロスポピュレーションの目的で外部キーの一部と見なされる列を制限しなければなりません。

Mutable Primary Keys / Update Cascades

エンティティの主キーが変更されると、その主キーを参照する関連アイテムも更新する必要があります。参照整合性を適用するデータベースでは、主キーの変更を参照先の外部キーに伝播するために、データベースの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バックエンドで、参照整合性をサポートする``InnoDB``エンジンが使用されることを保証する MySQL_engine='InnoDB' 設定についても説明しました。SQLiteを使用する場合は、 Foreign Key Support で説明されている設定を使用して、参照整合性を有効にする必要があります。

See also

Using foreign key ON DELETE cascade with ORM relationships - 関係を持つON DELETE CASCADEのサポート

mapper.passive_updates - Mapper と同様の機能です

Simulating limited ON UPDATE CASCADE without foreign key support

参照整合性をサポートしないデータベースが使用され、変更可能な値を持つ自然な主キーが使用されている場合、SQLAlchemyは、値が変更された主キー列をすぐに参照する外部キー列に対してUPDATE文を発行することによって、既に参照されている外部キーに 限定 の範囲で主キー値を伝播できる機能を提供します。参照整合性機能を持たない主なプラットフォームは、 MyISAM ストレージエンジンが使用されている場合はMySQLで、 PRAGMA foreign_keys=ON プラグマが使用されていない場合はSQLiteです。Oracleデータベースも ON UPDATE CASCADE をサポートしていませんが、参照整合性を強制するため、SQLAlchemyがUPDATE文を発行できるように、制約を遅延可能としてマークする必要があります。

この機能を有効にするには、 relationship.passive_updates フラグを False に設定します。もっとも望ましいのは、1対多または多対多の relationship() に設定することです。”updates”がもはや”passive”ではない場合、これは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"))
  • SELECTを使用して影響を受けるコレクションを完全に事前ロードする必要があり、それらの値に対してUPDATE文を発行する必要があるため、直接データベースのON UPDATE CASCADEよりもパフォーマンスが大幅に低下します。UPDATE文は”バッチ”で実行されますが、DBAPIレベルではローごとに実行されます。

  • 機能は一つ以上のレベルを”カスケード”することはできません。つまり、マッピングXがマッピングYの主キーを参照する外部キーを持っていても、マッピングYの主キー自体がマッピングZの外部キーである場合、 passive_updates=False は主キーの値の変更を Z から X にカスケードすることはできません。

  • 関係の多対1の側でのみ passive_updates=False を設定しても、完全な効果はありません。作業単位は、データベース全体ではなく、変化する主キーを持つものを参照している可能性のあるオブジェクトを、現在のIDマップを通じてのみ検索するからです。

Oracle以外の実質的に全てのデータベースが ON UPDATE CASCADE をサポートするようになりましたので、自然で変更可能なプライマリキー値が使用されている場合には、従来の ON UPDATE CASCADE サポートを使用することを強く推奨します。