きり丸の技術日記

技術検証したり、資格等をここに残していきます。

SQLModelで親と一緒に子テーブルを削除する(cascade_delete, ondelete)

始めに

外部キー制約があるレコードを削除するとき、参照元テーブルよりも参照先テーブルを先に削除する必要があります。

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

実装

Relationshipcascade_delete属性を付与するだけです。また、外部キー制約がないDBを使用していた際に、DBで直接削除された時用にFieldondelete属性を付与しておくとより安全に処理できます。

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オプションがあるのも面白いです。

こういう細かい部分を知っていくのは面白いですね。

参考情報