Cascades

マッパーは、 relationship() 構造体で設定可能な cascade 動作の概念をサポートしています。これは、特定の Session に関連する”parent”オブジェクトに対して実行される操作が、その関係によって参照される項目(例えば”child”オブジェクト)にどのように伝播されるかを参照し、 relationship.cascade オプションの影響を受けます。

カスケードのデフォルトの動作は、いわゆる save-update および merge 設定のカスケードに制限されています。カスケードの典型的な「代替」設定は、 delete および delete-orphan オプションを追加することです。これらの設定は、親にアタッチされている限り存在し、それ以外の場合は削除される関連オブジェクトに適しています。

カスケード動作は relationship()relationship.cascade オプションを使って設定されます:

class Order(Base):
    __tablename__ = "order"

    items = relationship("Item", cascade="all, delete-orphan")
    customer = relationship("User", cascade="save-update")

backrefにカスケードを設定するには、同じフラグを backref() 関数で使うことができます。この関数は最終的に引数を relationship() に返します:

class Item(Base):
    __tablename__ = "item"

    order = relationship(
        "Order", backref=backref("items", cascade="all, delete-orphan")
    )

relationship.cascade のデフォルト値は save-update, merge です。このパラメータの典型的な代替設定は all か、より一般的には all, delete-orphan です。 all 記号は save-update, merge, refresh-expire, expunge, delete の同義語で、 delete-orphan と一緒に使うと、子オブジェクトはどんな場合でも親と一緒に続き、親との関連がなくなると削除されることを示します。

Warning

all カスケードオプションは refresh-expire カスケード設定を意味します

しかし Asynchronous I/O (asyncio) 拡張を使用する場合には望ましくない可能性があります。 なぜなら、これは明示的なIOコンテキストで一般的に適切であるよりも積極的に関連オブジェクトを期限切れにするからです。さらなる背景については Preventing Implicit IO when Using AsyncSession の注を参照してください。

relationship.cascade パラメータに指定できる利用可能な値のリストは、以下のサブセクションで説明します。

save-update

Sessionuser1 を追加すると、 address1address2 も暗黙的に追加されます。

>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True

save-update カスケードは、すでに Session に存在するオブジェクトの属性操作にも影響します。3番目のオブジェクト、 address3user1.addresses コレクションに追加すると、それはその Session の状態の一部になります:

>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True

save-update カスケードは、コレクションから項目を削除したり、スカラー属性からオブジェクトの関連付けを解除したりするときに、驚くべき動作を示すことがあります。場合によっては、孤立したオブジェクトが元の親の Session に引き入れられることもあります。これは、フラッシュプロセスがその関連オブジェクトを適切に処理できるようにするためです。通常、このケースは、オブジェクトがある Session から削除され、別の Session に追加された場合にのみ発生します:

>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1 = user1.addresses[0]
>>> sess1.close()  # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1)  # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1)  # ... but it still gets added to the new session,
>>> address1 in sess2  # because it's still "pending" for flush
True

save-update カスケードはデフォルトで有効になっており、通常は当然のことと考えられています。 Session.add() を1回呼び出すだけで、その Session 内のオブジェクトの構造全体を一度に登録できるので、コードが簡単になります。無効にすることもできますが、通常は無効にする必要はありません。

Behavior of save-update cascade with bi-directional relationships

save-update カスケードは、双方向の関係の中で**一方向に**行われます。例えば relationship.back_populates または relationship.backref パラメータを使用して、互いを参照する2つの別々の relationship() オブジェクトを作成する場合などです。

Session に関連付けられていないオブジェクトは、 Session に関連付けられている親オブジェクトの属性またはコレクションに割り当てられると、同じ Session に自動的に追加されます。ただし、逆の同じ操作ではこの効果はありません。 Session に関連付けられている子オブジェクトが割り当てられている、 Session に関連付けられていないオブジェクトは、その親オブジェクトを Session に自動的に追加することはありません。この動作の全体的な主題は”cascade backrefs”として知られており、SQLAlchemy 2.0で標準化された動作の変更を表しています。

例を挙げると、 Order.itemsItem.order という関係を介して一連の Item オブジェクトに双方向に関連する Order オブジェクトのマッピングがあるとします。:

mapper_registry.map_imperatively(
    Order,
    order_table,
    properties={"items": relationship(Item, back_populates="order")},
)

mapper_registry.map_imperatively(
    Item,
    item_table,
    properties={"order": relationship(Order, back_populates="items")},
)

Order がすでに Session に関連付けられていて、 Item オブジェクトが作成され、その OrderOrder.items コレクションに追加された場合、 Item は自動的に同じ Session にカスケードされます。:

>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True

上記では、 Order.itemsItem.order の双方向の性質は、 Order.items に追加すると Item.order にも割り当てられることを意味します。同時に、 save-update カスケードによって、親の Order がすでに関連付けられているのと同じ SessionItem オブジェクトを追加することができました。

しかし、上記の操作が 方向で実行された場合、つまり、直接 Order.item に追加されるのではなく Item.order が割り当てられた場合、オブジェクトの割り当て Order.itemsItem.order が前の例と同じ状態になっても、 Session へのカスケード操作は自動的には 行われません 。:

>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False

上の例では、 Item オブジェクトが作成され、すべての状態が設定された後、 Session に明示的に追加されます:

>>> session.add(i1)

古いバージョンのSQLAlchemyでは、save-updateカスケードはすべてのケースで双方向に行われていました。その後、 cascade_backrefs として知られるオプションを使用してオプションになりました。最後に、SQLAlchemy 1.4では古い動作は推奨されなくなり、 cascade_backrefs オプションはSQLAlchemy 2.0で削除されました。その理論的根拠は、上で i1.order=o1 の代入として説明したように、オブジェクトの属性に代入すると、そのオブジェクトの持続状態が Session 内で保留されるように変更され、その後、指定されたオブジェクトがまだ構築中でフラッシュの準備ができていない場合に、autoflushがオブジェクトを時期尚早にフラッシュしてエラーを引き起こすという問題が頻繁に発生するということです。単一方向と双方向の動作を選択するオプションも削除されました。このオプションは、ORMの全体的な学習曲線とドキュメントとユーザサポートの負担に加えて、2つのわずかに異なる作業方法を作成したからです。

See also

cascade_backrefs behavior deprecated for removal in 2.0 - “cascade backrefs”の動作の変更に関する背景

delete

delete カスケードは、”parent”オブジェクトが削除対象としてマークされている場合、それに関連する”child”オブジェクトも削除対象としてマークされるべきであることを示します。例えば、 delete カスケードが設定された User.addresses という関係があるとします。:

class User(Base):
    # ...

    addresses = relationship("Address", cascade="all, delete")

上記のマッピングを使用すると、 User オブジェクトと2つの関連する Address オブジェクトが得られます。:

>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses

user1 に削除のマークを付けると、フラッシュ操作が進んだ後、 address1address2 も削除されます。

>>> sess.delete(user1)
>>> sess.commit()
DELETE FROM address WHERE address.id = ? ((1,), (2,)) DELETE FROM user WHERE user.id = ? (1,) COMMIT

あるいは、 User.addresses という関係に delete というカスケードが 存在しない 場合、SQLAlchemyのデフォルトの動作は、 address1address2 の外部キー参照を NULL に設定することで、 user1 から address1address2 の関連付けを解除することです。次のようなマッピングを使用します。:

class User(Base):
    # ...

    addresses = relationship("Address")

親の User オブジェクトを削除しても、 address の行は削除されず、関連付けが解除されます。

>>> sess.delete(user1)
>>> sess.commit()
UPDATE address SET user_id=? WHERE address.id = ? (None, 1) UPDATE address SET user_id=? WHERE address.id = ? (None, 2) DELETE FROM user WHERE user.id = ? (1,) COMMIT

一対多リレーションシップの delete カスケードは、 delete-orphan カスケードと組み合わされることがよくあります。これは、”child”オブジェクトが親から関連付けを解除された場合に、関連する行に対してDELETEを発行します。 deletedelete-orphan カスケードの組み合わせは、SQLAlchemyが外部キー列をNULLに設定するか、行を完全に削除するかを決定しなければならない両方の状況をカバーします。

デフォルトでは、この機能はデータベースに設定された FOREIGN KEY 制約とは完全に独立して動作します。この制約自体が CASCADE の動作を設定します。この設定とより効率的に統合するためには、 Using foreign key ON DELETE cascade with ORM relationships で説明されている追加のディレクティブを使用する必要があります。

Warning

ORMの”delete”と”delete-orphan”の動作は、 Session.delete() メソッドを使用して、 unit of work プロセス内で削除する個々のORMインスタンスをマークする場合に のみ適用されますORM UPDATE and DELETE with Custom WHERE Criteria に示されているように、 delete() 構文を使用して生成される “バルク” 削除には適用されません。 Important Notes and Caveats for ORM-Enabled Update and Delete を参照してください。

Using delete cascade with many-to-many relationships

relationship.secondary を使用して関連付けテーブルを示す多対多の関係でも、 cascade="all, delete" オプションは同じように機能します。親オブジェクトが削除され、関連するオブジェクトとの関連付けが解除されると、作業単位プロセスは通常、関連テーブルから行を削除しますが、関連するオブジェクトはそのままにしておきます。 cascade="all, delete" と組み合わせると、子の行自体に対して追加の DELETE 文が実行されます。

次の例では、 Many To Many の例を応用して、関連付けの 一方 側の cascade="all, delete" 設定を示しています。:

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id")),
    Column("right_id", Integer, ForeignKey("right.id")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
    )

上の例では、 Session.delete() を使って削除対象として Parent オブジェクトを指定すると、フラッシュプロセスは通常のように関連する行を association テーブルから削除しますが、カスケードルールによっては関連する Child 行もすべて削除されます。

Warning

上記の cascade="all, delete" 設定が both 関係に設定されている場合、カスケードアクションはすべての Parent オブジェクトと Child オブジェクトをカスケードし、検出された各 children コレクションと parents コレクションをロードし、接続されているものをすべて削除します。通常、”delete”カスケードが双方向に設定されることは望ましくありません。

Using foreign key ON DELETE cascade with ORM relationships

SQLAlchemyの 削除 カスケードの動作は、データベースの ForeignKey 制約の ON DELETE 機能と重複します。SQLAlchemyでは、これらのスキーマレベルの DDL の動作を、 ForeignKey および ForeignKeyConstraint 構文を使って設定できます。これらのオブジェクトを Table メタデータと組み合わせて使用する方法については、 ON UPDATE and ON DELETE で説明しています。

relationship() には追加のオプションがあります。これは、ORMが関連する行自身に対してDELETE/UPDATE操作をどの程度実行しようとするかを示します。これは、データベース側のFOREIGN KEY制約カスケードがタスクを処理するためにどの程度依存すべきかを示します。これは relationship.passive_deletes パラメータで、オプションとして False (デフォルト)、 True および all を受け入れます。

最も典型的な例は、親の行が削除されたときに子の行が削除される場合で、関連する FOREIGN KEY 制約にも ON DELETE CASCADE が設定されている場合です。:

class Parent(Base):
    __tablename__ = "parent"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        back_populates="parent",
        cascade="all, delete",
        passive_deletes=True,
    )

class Child(Base):
    __tablename__ = "child"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
    parent = relationship("Parent", back_populates="children")

親行が削除されたときの上記の設定の動作は次のとおりです。:

  1. アプリケーションは session.delete(my_parent) を呼び出します。ここで my_parentParent のインスタンスです。

  2. When the Session next flushes changes to the database, all of the currently loaded items within the my_parent.children collection are deleted by the ORM, meaning a DELETE statement is emitted for each record.

  1. Session が次に変更をデータベースにフラッシュするとき、 my_parent.children コレクション内の 現在ロードされている すべての項目がORMによって削除されます。つまり、各レコードに対して DELETE 文が発行されます。

  1. my_parent.children コレクションが unloaded の場合、 DELETE 文は生成されません。 relationship.passive_deletes フラグがこの relationship()設定されていない 場合、アンロードされた Child オブジェクトに対して SELECT 文が生成されます。

  1. my_parent 行自体に対して DELETE 文が発行されます。

  1. データベースレベルの ON DELETE CASCADE 設定では、 parent の影響を受ける行を参照する「child」のすべての行も削除されます。

  2. The Parent instance referred to by my_parent, as well as all instances of Child that were related to this object and were loaded (i.e. step 2 above took place), are de-associated from the Session.

  1. my_parent によって参照される Parent インスタンスと、このオブジェクトに関連し、 ロード された(つまり、上記の手順2が実行された) Child のすべてのインスタンスは、 Session から関連付けが解除されます。

Note

“ON DELETE CASCADE”を使用するためには、基礎となるデータベースエンジンが FOREIGN KEY 制約をサポートしていて、以下を実行している必要があります。:

  • SQLiteを使用する場合、外部キーのサポートを明示的に有効にする必要があります。詳細は Foreign Key Support を参照してください。

Using foreign key ON DELETE with many-to-many relationships

Using delete cascade with many-to-many relationships で説明されているように、”delete”カスケードは多対多の関係に対しても同様に機能します。多対多と共に ON DELETE CASCADE 外部キーを利用するために、関連付けテーブルには FOREIGN KEY ディレクティブが設定されています。これらのディレクティブは関連付けテーブルから自動的に削除するタスクを処理できますが、関連するオブジェクト自体の自動的な削除には対応できません。

この場合、 relationship.passive_deletes ディレクティブを使用すると、削除操作中に追加の SELECT 文をいくつか節約できますが、影響を受ける子オブジェクトを見つけて正しく処理するために、ORMがロードし続けるコレクションがまだいくつかあります。

Note

これに対する仮想的な最適化には、関連付けテーブルの親に関連付けられたすべての行に対する単一の DELETE 文を一度に含むことができ、その後、影響を受ける関連する子行を見つけるために RETURNING を使用しますが、これは現在のところORMの作業単位実装の一部ではありません。

この設定では、関連テーブルの両方の外部キー制約に対して ON DELETE CASCADE を設定します。関係の親->子側に対して``cascade=”all, delete”`` を設定し、双方向関係の 側に対して passive_deletes=True を設定します。:

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
    Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
        passive_deletes=True,
    )

上記の設定を使用すると、 Parent オブジェクトの削除は次のように行われます。

  1. Session.delete() を使って`Parent`オブジェクトに削除マークを付けます。

  1. フラッシュが発生したときに、 Parent.children コレクションがロードされていなければ、ORMはまずSELECT文を発行して、 Parent.children``に対応する ``Child オブジェクトをロードします。

  1. その後、その親行に対応する association の行に対して DELETE 文を発行します。

  1. この即時削除によって影響を受ける各 Child オブジェクトに対して、 passive_deletes=True が設定されているので、作業単位は各 Child.parents コレクションに対してSELECT文を出力しようとする必要はありません。なぜなら、 association の対応する行が削除されると想定されるからです。

  1. DELETE 文は、 Parent.children からロードされた Child オブジェクトごとに生成されます。

delete-orphan

delete-orphan カスケードは、 delete カスケードに動作を追加します。これにより、親が削除対象としてマークされている場合だけでなく、親との関連付けが解除された場合にも、子オブジェクトが削除対象としてマークされるようになります。これは、NOT NULL外部キーを使用して、親によって「所有」されている関連オブジェクトを処理する場合によく見られる機能であり、親コレクションからアイテムを削除すると削除されます。

delete-orphan カスケードは、各子オブジェクトが一度に1つの親しか持つことができないことを意味し、 ほとんどの場合、1対多の関係のみに設定されます 。多対1または多対多の関係に設定するあまり一般的でないケースでは、 relationship.single_parent 引数を設定することで、”多”側で一度に1つのオブジェクトのみを許可するように強制できます。これにより、オブジェクトが一度に1つの親のみに関連付けられることを保証するPython側の検証が確立されますが、これは”多”関係の機能を大幅に制限し、通常は望ましいものではありません。

merge

merge カスケードは、 Session.merge() 操作が、 Session.merge() 呼び出しの対象である親から参照されるオブジェクトに伝播されることを示します。このカスケードもデフォルトでオンになっています。

refresh-expire

refresh-expire は一般的ではないオプションで、 Session.expire() 操作が親から参照されたオブジェクトに伝播されることを示します。 Session.refresh() を使用すると、参照されたオブジェクトは期限切れになりますが、実際には更新されません。

expunge

expunge カスケードは、親オブジェクトが Session.expunge() を使って Session から削除された時に、操作が参照されたオブジェクトに伝搬されるべきであることを示します。

Notes on Delete - Deleting Objects Referenced from Collections and Scalar Relationships

一般に、ORMはフラッシュ処理中にコレクションまたはスカラー関係の内容を変更しません。つまり、クラスにオブジェクトのコレクションを参照する relationship() がある場合、または多対1などの単一オブジェクトへの参照がある場合、フラッシュ処理が発生してもこの属性の内容は変更されません。代わりに、 Session は、 Session.commit() のexpire-on-commit動作、または Session.expire() の明示的な使用によって、最終的に期限切れになることが予想されます。その時点で、その Session に関連付けられた参照オブジェクトまたはコレクションはクリアされ、次のアクセス時に再ロードされます。

この動作に関してよくある混乱は、 Session.delete() メソッドの使用に関係しています。オブジェクトに対して Session.delete() が呼び出され、 Session がフラッシュされると、その行はデータベースから削除されます。外部キーを介してターゲット行を参照する行は、2つのマップされたオブジェクト型の間で relationship() を使用して追跡されていると仮定すると、その外部キー属性もNULLに更新されたと見なされます。または、削除カスケードが設定されている場合は、関連する行も削除されます。ただし、削除されたオブジェクトに関連する行自体も変更される可能性がありますが、 フラッシュ自体の範囲内での操作に含まれる オブジェクト上のリレーションシップにバインドされたコレクションまたはオブジェクト参照には変更は発生しません。これは、オブジェクトが関連するコレクションのメンバーであった場合、そのコレクションが期限切れになるまでPython側に存在し続けることを意味します。同様に、オブジェクトが別のオブジェクトから多対1または1対1で参照されていた場合、その参照はオブジェクトが期限切れになるまでそのオブジェクト上に存在し続けることになります。

以下では、 Address オブジェクトが削除対象としてマークされた後も、親の User に関連付けられたコレクションには、フラッシュ後も存在していることを示します。:

>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

上記のセッションがコミットされると、すべての属性が期限切れになります。次に user.addresses にアクセスすると、コレクションが再ロードされ、目的の状態が表示されます。:

>>> session.commit()
>>> address in user.addresses
False

Session.delete() をインターセプトし、この有効期限を自動的に呼び出すレシピがあります。これについては、 ExpireRelationshipOnFKChange を参照してください。しかし、コレクション内の項目を削除する通常の方法は、 Session.delete() を直接使用するのではなく、カスケード動作を使用して、親コレクションからオブジェクトを削除した結果として自動的に削除を呼び出すことです。以下の例に示すように、delete-orphan カスケードはこれを実現します。

class User(Base):

__tablename__ = “user”

# …

addresses = relationship(“Address”, cascade=”all, delete-orphan”)

# …

del user.addresses[1] session.flush()

上記の場合、 User.addresses コレクションから Address オブジェクトを削除すると、 delete-orphan カスケードは、 Session.delete() に渡すのと同じ方法で、 Address オブジェクトに削除のマークを付ける効果があります。

delete-orphan カスケードは、多対1または1対1の関係にも適用できるので、オブジェクトが親から関連解除されると、自動的に削除対象としてマークされます。多対1または1対1で delete-orphan カスケードを使用するには、追加のフラグ relationship.single_parent が必要です。このフラグは、この関連オブジェクトが他の親と同時に共有されないというアサーションを呼び出します:

class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete-orphan", single_parent=True
    )

上記では、仮の Preference オブジェクトが User から削除されると、フラッシュ時に削除されます:

some_user.preference = None
session.flush()  # will delete the Preference object

See also

Cascades カスケードの詳細について。