始めに
外部キー制約があるレコードを削除するとき、参照元テーブルよりも参照先テーブルを先に削除する必要があります。
Railsの場合、次のようにdependent
に定義しておくと、Parent
テーブルを削除したタイミングでChild
テーブルも削除されます。
class Parent < ApplicationRecord has_one :child, dependent: :destroy end class Child < ApplicationRecord belongs_to :parent end
今回の記事では、PythonのSQLModelを使用したときに、Parent
テーブルを削除したタイミングでChild
テーブルも削除されるようにします。
環境
- Python
- 3.12.4
- FastAPI
- 0.112.1
- SQLModel
- 0.0.21
実装
Relationship
のcascade_delete
属性を付与するだけです。また、外部キー制約がないDBを使用していた際に、DBで直接削除された時用にField
のondelete
属性を付与しておくとより安全に処理できます。
from sqlmodel import SQLModel, Field, Relationship class Parent(SQLModel, table=True): __tablename__ = "parents" id: int = Field(primary_key=True) child: "Child" = Relationship(back_populates="parent", cascade_delete=True, sa_relationship_kwargs={"uselist": False}) class Child(SQLModel, table=True): __tablename__ = "childs" id: int = Field(foreign_key="parents.id", primary_key=True, ondelete="CASCADE") parent: "Parent" = Relationship(back_populates="child")
こちらを定義するだけで、Parent
のテーブルのモデルを削除したタイミングで一緒に削除してくれます。
async def test_01(self, db: AsyncSession) -> None: parent = Parent(id=1) db.add(task1) child = Child(id=1) db.add(done1) await db.commit() # WHEN print("XXXXXX") await db.delete(parent) await db.commit() print("XXXXXX") # THEN query: select = select(Child) actual = (await db.execute(query)).scalars().all() assert len(actual) == 0
ログを見ればParent
を削除したタイミングでChild
もforeign_keyをもとに検索をかけていることがわかり、Child
, Parent
の順番で削除されていることがわかります。
2024-08-25 21:52:39,968 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2024-08-25 21:52:39,970 INFO sqlalchemy.engine.Engine SELECT parents.id FROM parents WHERE parents.id = ? 2024-08-25 21:52:39,970 INFO sqlalchemy.engine.Engine [generated in 0.00021s] (1,) 2024-08-25 21:52:39,971 INFO sqlalchemy.engine.Engine SELECT childs.id FROM childs WHERE childs.id = ? 2024-08-25 21:52:39,971 INFO sqlalchemy.engine.Engine [generated in 0.00016s] (1,) 2024-08-25 21:52:39,973 INFO sqlalchemy.engine.Engine DELETE FROM childs WHERE childs.id = ? 2024-08-25 21:52:39,973 INFO sqlalchemy.engine.Engine [generated in 0.00017s] (1,) 2024-08-25 21:52:39,974 INFO sqlalchemy.engine.Engine DELETE FROM parents WHERE parents.id = ? 2024-08-25 21:52:39,974 INFO sqlalchemy.engine.Engine [generated in 0.00014s] (1,) 2024-08-25 21:52:39,975 INFO sqlalchemy.engine.Engine COMMIT
注意点
あくまでインスタンス経由で削除する場合に効く構文なので、直接Delete文を発行する処理には効きません。素直にデータベースの構文を使いましょう。ondelete
を設定していればできると思ったのですがダメでした。
query = delete(Task).where(Task.id == 1) _ = await db.execute(query)
ソースコード
終わりに
こういう処理ができると直接SQLを発行しないでORMを経由するメリットがありますね。また、SQLAlchemy
ではできなかったDB
側で直接削除された時用のondelete
オプションがあるのも面白いです。
こういう細かい部分を知っていくのは面白いですね。