DjangoのModel.delete()
のオーバーライド時の注意点
更新
- 2023/3:
connect
はsignals.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_delete
やpost_delete
というsignalがある。今回は削除後のsignalであるpost_delete
を使う。
signalを使う場合にしなければならないことは、
- コールバック関数を用意して、
@receiver
デコレータを付ける。 - 使っているだろう
AppConfig
のサブクラス(A)にて、ready()
をオーバーライドして、その中でsignalに対応するコールバック関数を登録する。 - 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()
のオーバーライド:
signalについてのドキュメント2つ: