Djangoのsignals.pre_deleteとpost_delete

やりたいこと

あるモデルのレコードが削除されたとき、なんからの処理を行いたい

やりかた

Djangoの場合はsignalという仕組みが用意されています


例えばこのようにモデルとsignalsの設定をします

from django.db import models
from django.db.models.signals import pre_delete, post_delete

class Author(models.Model):
    name = models.CharField(max_length=100)

class Entry(models.Model):
    title = models.CharField(max_length=100)
    body = models.CharField(max_length=1000)
    author = models.ForeignKey(Author)


def pre_author_delete(sender, instance, **kwargs):
    print "pre_author_delete : %s" % instance.name

def post_author_delete(sender, instance, **kwargs):
    print "post_author_delete : %s" % instance.name

def pre_entry_delete(sender, instance, **kwargs):
    print "pre_entry_delete: : %s" % instance.title

def post_entry_delete(sender, instance, **kwargs):
    print "post_entry_delete : %s" % instance.title

コンソールからAuthorを1つと関連するEntryを3つ作成します

>>> from app.models import *
>>> author = Author.objects.create(name='me')
>>> Entry.objects.create(author=author, title='title1', body='body1')
Out[6]: <Entry: Entry object>
>>> Entry.objects.create(author=author, title='title2', body='body2')
Out[7]: <Entry: Entry object>
>>> Entry.objects.create(author=author, title='title3', body='body3')
Out[8]: <Entry: Entry object>


Entryをひとつ削除してみます。

>>> Entry.objects.get(title='title3').delete()
pre_entry_delete: : title3
post_entry_delete : title3

pre_entry_deleteとpost_entry_deleteが呼ばれるのがわかります


Authorを削除してみます。

>>> Author.objects.get(name='me').delete()
pre_entry_delete: : title1
pre_entry_delete: : title2
pre_author_delete : me
post_entry_delete : title2
post_entry_delete : title1
post_author_delete : me


関連モデルである、Entryもすべて削除されるので、Authorに対して1回、残っているEntry2つに対しても1回ずつpre_deleteとpost_deleteが呼ばれます

pre_deleteや、post_deleteでエラーが発生した場合はどうなるか


やってみます

pre_deleteの処理をちょっと変更して、データを登録し直します。

def pre_author_delete(sender, instance, **kwargs):
    if True:
        raise Exception('error')
    print "pre_author_delete : %s" % instance.name

def post_author_delete(sender, instance, **kwargs):
    print "post_author_delete : %s" % instance.name

def pre_entry_delete(sender, instance, **kwargs):
    print "pre_entry_delete: : %s" % instance.title

def post_entry_delete(sender, instance, **kwargs):
    print "post_entry_delete : %s" % instance.title


Authorを削除してみます。

In [5]: author.delete()
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 author.delete()

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/base.pyc in delete(self, using)
    574         collector = Collector(using=using)
    575         collector.collect([self])
--> 576         collector.delete()
    577 
    578     delete.alters_data = True

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in decorated(self, *args, **kwargs)
     59             forced_managed = False
     60         try:
---> 61             func(self, *args, **kwargs)
     62             if forced_managed:
     63                 transaction.commit(using=self.using)

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in delete(self)
    237             if not model._meta.auto_created:
    238                 signals.pre_delete.send(
--> 239                     sender=model, instance=obj, using=self.using
    240                 )
    241 

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named)
    170 
    171         for receiver in self._live_receivers(_make_id(sender)):
--> 172             response = receiver(signal=self, sender=sender, **named)
    173             responses.append((receiver, response))
    174         return responses

/Users/yuhei/Dropbox/workspace/django_signals/app/models.py in pre_author_delete(sender, instance, **kwargs)
     15 def pre_author_delete(sender, instance, **kwargs):
     16     if True:
---> 17         raise Exception('error')
     18     print "pre_author_delete : %s" % instance.name
     19 

Exception: error

In [6]: Author.objects.all()
Out[6]: [<Author: Author object>]

もちろんエラーが発生します。そしてデータの削除は実行されません。


こんどはpost_deleteの処理でエラーが発生するようにしてみます

def pre_author_delete(sender, instance, **kwargs):
    print "pre_author_delete : %s" % instance.name

def post_author_delete(sender, instance, **kwargs):
    if True:
        raise Exception('error')
    print "post_author_delete : %s" % instance.name

def pre_entry_delete(sender, instance, **kwargs):
    print "pre_entry_delete: : %s" % instance.title

def post_entry_delete(sender, instance, **kwargs):
    print "post_entry_delete : %s" % instance.title
In [3]: author.delete()
pre_author_delete : me
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 author.delete()

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/base.pyc in delete(self, using)
    574         collector = Collector(using=using)
    575         collector.collect([self])
--> 576         collector.delete()
    577 
    578     delete.alters_data = True

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in decorated(self, *args, **kwargs)
     59             forced_managed = False
     60         try:
---> 61             func(self, *args, **kwargs)
     62             if forced_managed:
     63                 transaction.commit(using=self.using)

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in delete(self)
    267             if not model._meta.auto_created:
    268                 signals.post_delete.send(
--> 269                     sender=model, instance=obj, using=self.using
    270                 )
    271 

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named)
    170 
    171         for receiver in self._live_receivers(_make_id(sender)):
--> 172             response = receiver(signal=self, sender=sender, **named)
    173             responses.append((receiver, response))
    174         return responses

/Users/yuhei/Dropbox/workspace/django_signals/app/models.py in post_author_delete(sender, instance, **kwargs)
     18 def post_author_delete(sender, instance, **kwargs):
     19     if True:
---> 20         raise Exception('error')
     21     print "post_author_delete : %s" % instance.name
     22 

Exception: error

In [4]: Author.objects.all()
Out[4]: []

もちろんエラーが発生しますが、Author.objects.all()はモデルを返さなくなるので、削除自体は完了してるように見えます。
ところがDBにはレコードが残ってます。

shellを立ち上げ直すとAuthor.objects.all()でしっかり値が返ってきます。

これはどういう状況なんでしょうか?


ちょっと調べてもよくわからなかったので宿題にします。
とりあえず動きとしては「post_deleteの処理でエラーが発生した場合、削除処理はロールバックされる」ようです。


では関連オブジェクトの削除でpre_deleteがエラーになった場合はAuthorは削除されるでしょうか?

def pre_author_delete(sender, instance, **kwargs):
    print "pre_author_delete : %s" % instance.name

def post_author_delete(sender, instance, **kwargs):
    print "post_author_delete : %s" % instance.name

def pre_entry_delete(sender, instance, **kwargs):
    if True:
        raise Exception('error')
    print "pre_entry_delete: : %s" % instance.title

def post_entry_delete(sender, instance, **kwargs):
    print "post_entry_delete : %s" % instance.title
In [4]: author.delete()
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 author.delete()

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/base.pyc in delete(self, using)
    574         collector = Collector(using=using)
    575         collector.collect([self])
--> 576         collector.delete()
    577 
    578     delete.alters_data = True

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in decorated(self, *args, **kwargs)
     59             forced_managed = False
     60         try:
---> 61             func(self, *args, **kwargs)
     62             if forced_managed:
     63                 transaction.commit(using=self.using)

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in delete(self)
    237             if not model._meta.auto_created:
    238                 signals.pre_delete.send(
--> 239                     sender=model, instance=obj, using=self.using
    240                 )
    241 

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named)
    170 
    171         for receiver in self._live_receivers(_make_id(sender)):
--> 172             response = receiver(signal=self, sender=sender, **named)
    173             responses.append((receiver, response))
    174         return responses

/Users/yuhei/Dropbox/workspace/django_signals/app/models.pyc in pre_entry_delete(sender, instance, **kwargs)
     21 def pre_entry_delete(sender, instance, **kwargs):
     22     if True:
---> 23         raise Exception('error')
     24     print "pre_entry_delete: : %s" % instance.title
     25 

Exception: error

In [5]: Author.objects.all()
Out[5]: [<Author: Author object>]

In [6]: Entry.objects.all()
Out[6]: [<Entry: Entry object>, <Entry: Entry object>]


この場合、データはまったく消えてません。関連オブジェクトの削除にひもづけられたpre_deleteがエラーになった場合、元のdeleteのレシーバオブジェクトの削除は実行されません


post_deleteでも同様でしょうか?

def pre_author_delete(sender, instance, **kwargs):
    print "pre_author_delete : %s" % instance.name

def post_author_delete(sender, instance, **kwargs):
    print "post_author_delete : %s" % instance.name

def pre_entry_delete(sender, instance, **kwargs):
    print "pre_entry_delete: : %s" % instance.title

def post_entry_delete(sender, instance, **kwargs):
    if True:
        raise Exception('error')
    print "post_entry_delete : %s" % instance.title
In [3]: author.delete()
pre_entry_delete: : title1
pre_entry_delete: : title2
pre_author_delete : yuhei
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 author.delete()

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/base.pyc in delete(self, using)
    574         collector = Collector(using=using)
    575         collector.collect([self])
--> 576         collector.delete()
    577 
    578     delete.alters_data = True

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in decorated(self, *args, **kwargs)
     59             forced_managed = False
     60         try:
---> 61             func(self, *args, **kwargs)
     62             if forced_managed:
     63                 transaction.commit(using=self.using)

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/db/models/deletion.pyc in delete(self)
    267             if not model._meta.auto_created:
    268                 signals.post_delete.send(
--> 269                     sender=model, instance=obj, using=self.using
    270                 )
    271 

/Users/yuhei/.virtualenvs/django14/lib/python2.7/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named)
    170 
    171         for receiver in self._live_receivers(_make_id(sender)):
--> 172             response = receiver(signal=self, sender=sender, **named)
    173             responses.append((receiver, response))
    174         return responses

/Users/yuhei/Dropbox/workspace/django_signals/app/models.py in post_entry_delete(sender, instance, **kwargs)
     24 def post_entry_delete(sender, instance, **kwargs):
     25     if True:
---> 26         raise Exception('error')
     27     print "post_entry_delete : %s" % instance.title
     28 

Exception: error

In [4]: Author.objects.all()
Out[4]: []

In [5]: Entry.objects.all()
Out[5]: []


Author.objects.all()やEntry.objects.all()は値を返さなくなります。しかし、例によってdbには値が残ってて、shellを立ち上げ直すと値が取得できます。

まとめ

djangoのsignals.pre_deleteとpost_deleteの動きがだいたいつかめた。
ただpost_deleteでエラーが発生した場合、そのセッション中は見えなくなるが、DBには値が残っているというのがよくわからない。postgresとmysqlで同じ挙動だった。