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¶
Session
に user1
を追加すると、 address1
、 address2
も暗黙的に追加されます。
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True
save-update
カスケードは、すでに Session
に存在するオブジェクトの属性操作にも影響します。3番目のオブジェクト、 address3
を user1.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.items
と Item.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
オブジェクトが作成され、その Order
の Order.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.items
と Item.order
の双方向の性質は、 Order.items
に追加すると Item.order
にも割り当てられることを意味します。同時に、 save-update
カスケードによって、親の Order
がすでに関連付けられているのと同じ Session
に Item
オブジェクトを追加することができました。
しかし、上記の操作が 逆 方向で実行された場合、つまり、直接 Order.item
に追加されるのではなく Item.order
が割り当てられた場合、オブジェクトの割り当て Order.items
と Item.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
に削除のマークを付けると、フラッシュ操作が進んだ後、 address1
と address2
も削除されます。
>>> 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のデフォルトの動作は、 address1
と address2
の外部キー参照を NULL
に設定することで、 user1
から address1
と address2
の関連付けを解除することです。次のようなマッピングを使用します。:
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を発行します。 delete
と delete-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 を参照してください。
See also
Using foreign key ON DELETE cascade with ORM relationships
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")
親行が削除されたときの上記の設定の動作は次のとおりです。:
アプリケーションは
session.delete(my_parent)
を呼び出します。ここでmy_parent
はParent
のインスタンスです。When the
Session
next flushes changes to the database, all of the currently loaded items within themy_parent.children
collection are deleted by the ORM, meaning aDELETE
statement is emitted for each record.
Session
が次に変更をデータベースにフラッシュするとき、my_parent.children
コレクション内の 現在ロードされている すべての項目がORMによって削除されます。つまり、各レコードに対してDELETE
文が発行されます。
my_parent.children
コレクションが unloaded の場合、DELETE
文は生成されません。relationship.passive_deletes
フラグがこのrelationship()
に 設定されていない 場合、アンロードされたChild
オブジェクトに対してSELECT
文が生成されます。
my_parent
行自体に対してDELETE
文が発行されます。
データベースレベルの
ON DELETE CASCADE
設定では、parent
の影響を受ける行を参照する「child」のすべての行も削除されます。The
Parent
instance referred to bymy_parent
, as well as all instances ofChild
that were related to this object and were loaded (i.e. step 2 above took place), are de-associated from theSession
.
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
オブジェクトの削除は次のように行われます。
Session.delete()
を使って`Parent`オブジェクトに削除マークを付けます。
フラッシュが発生したときに、
Parent.children
コレクションがロードされていなければ、ORMはまずSELECT文を発行して、Parent.children``に対応する ``Child
オブジェクトをロードします。
その後、その親行に対応する
association
の行に対してDELETE
文を発行します。
この即時削除によって影響を受ける各
Child
オブジェクトに対して、passive_deletes=True
が設定されているので、作業単位は各Child.parents
コレクションに対してSELECT文を出力しようとする必要はありません。なぜなら、association
の対応する行が削除されると想定されるからです。
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 カスケードの詳細について。