DjangoでModel.delete()のオーバーライドはしなくてもいい

DjangoのModel.delete()のオーバーライド時の注意点

更新

  • 2023/3: connectsignals.pyを使っている場合、不要だったので記述を更新。

やりたいこと

Djangoにて、DBからのアイテムの削除後にやりたい処理があったため、Mymodel.delete()delete()をオーバーライドして、カスケード的に処理をしようと思った。

class Mymodel(models.Model):
    def delete(self, *args, **kwargs) -> Tuple[int, Dict[str, int]]:
        result = super().delete(*args, **kwargs)
        # 何かする
        return result

しかし、Mymodel.objects.all().delete()などでDBのアイテムを削除しても、オーバーライドしたメソッドの処理が実行されていなかった。

よく読む

Djangoのドキュメントに書いてあった:

Overridden model methods are not called on bulk operations

Note that the delete() method for an object is not necessarily called when deleting objects in bulk using a QuerySet or as a result of a cascading delete. To ensure customized delete logic gets executed, you can use pre_delete and/or post_delete signals.

Unfortunately, there isn’t a workaround when creating or updating objects in bulk, since none of save(), pre_save, and post_save are called.

deleteに関してはシグナルを利用すれば何とかなるようだ。

単純な解決策

個別にdelete()を呼び出して消去すればいい:

def delete_mymodel(request):
    # ...省略
    # フィルタを用意
    for i in Mymodel.objects.filter(this_is=somefilter):
        # オーバーライドしたメソッドを実行。
        i.delete()
    return HttpResponse()

個別にdelete()をして確実に行う。大量のデータを扱うときはパフォーマンスが落ちると思う。あとスマートではない気がする。

推奨されているシグナルを使う解決策

Djangoは発生した(する)イベントに応じてsignalを発していて、登録されているreceiver(コールバック)があればそれを実行してくれる機能がある。

Electronのinvokeのようなものだ。

このシグナルは、Mymodel.objects.all().delete()での削除でも、発火して、コールバック関数を呼ぶことができるので、より便利で、個別に呼ばなければならない気配りをしなくて済む。また、コールバック関数にMymodelのインスタンスも渡せるので、インスタンスに応じた処理を問題なく行うことができる。

デフォルトでDBのアイテムの削除時には、pre_deletepost_deleteというsignalがある。今回は削除後のsignalであるpost_deleteを使う。

signalを使う場合にしなければならないことは、

  1. コールバック関数を用意して、@receiverデコレータを付ける。
  2. 使っているだろうAppConfigのサブクラス(A)にて、ready()をオーバーライドして、その中でsignalに対応するコールバック関数を登録する。
  3. Django settingsのINSTALLED_APPSに上記のAが登録されていなければ、登録する。

この3点くらいだろう。以下、それぞれについて実装例を残す。

実装

1番目の項目は、signals.pyをAppルートに用意して(models.pyなどと兄弟になるように):

""" myapp/signals.py """
from django.db.models.signals import post_delete
from django.dispatch import receiver

from myapp.models import Mymodel


@receiver(post_delete, sender=Mymodel)
def mycallback(sender, instance: Mymodel, **kwargs):
    # Mymodelの削除によってこのコールバック関数が実行される。
    # instanceは削除されたMymodelのインスタンス。primary keyなどは無効な値になっているので注意

    # 何かする
    print("im callback, signal receiver")

このようにすればいい。signalは使い始めるとコードが散らかる可能性があるので、まとめておくといいと書いてあったと思う。

2番目の項目については、apps.pyにあると思う。(Pollチュートリアルで扱っていた気もする。)

dispatch_uidは、テスト時などでの多重登録を防ぐ:

""" myapp/apps.py """
from django.apps import AppConfig

class MyappConfig(AppConfig):
    # ... 省略

    def ready(self) -> None:
        # signalを使ってカスケードのような処理をする(コールバックを登録している)
        from myapp import signals
        # 下はいらない!
        # post_delete.connect(
        #     signals.mycallback, dispatch_uid="post_delete_mycallback"
        # )
        return super().ready()

こうしてconnectしないとコールバック関数を呼んでもらえないので注意したい。

importして読み込ませれば、大丈夫だった。@receiverデコレータが読まれて登録される。models.pyに書いて登録してもいいようだ。

3番目はsettingsについてはクラスやらインスタンスやらで多様な定義の仕方があるので、抜粋だけ:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "myapp",  # これが必要
    # ...省略
]

こうすることで2番目のreadyが実行される。

ちょっとセットアップがあるが、for文で回すことなくQuerySetでそのままdelete()できるようになる利点は大きい。こちらの方法がいいだろう。 ここまでのセットアップがうまくいっていれば:

def delete_mymodel(request):
    # ...省略
    # フィルタを用意
    Mymodel.objects.filter(this_is=somefilter).delete()
    return HttpResponse()

これだけでカスケード処理ができる。

おわり

Djangoはよくできていると感心しきり。英語のままの部分のドキュメントもしっかり読もう。

以上です。

参考

Model.delete()のオーバーライド:

Models | Django documentation
The web framework for perfectionists with deadlines.

signalについてのドキュメント2つ:

Signals | Django ドキュメント
The web framework for perfectionists with deadlines.
Signals | Django ドキュメント
The web framework for perfectionists with deadlines.
タイトルとURLをコピーしました