17

Django shows how to set or override cascade deletes with foreign keys in their documents.

model = models.ForeignKey(MyModel, null = True, on_delete = models.SET_NULL)

But what if we wanted this effect the other way around? What if we want the deletion of the fk model to result in the deletion of this model?

Thanks

Lucas Ou-Yang
  • 5,505
  • 13
  • 43
  • 62

2 Answers2

13

There is a very delicate implementation point, that I thought I should add to this discussion.

Let's say we have two models, one of which references the other one by a foreign key, as in:

class A(models.Model):
    x = models.IntegerField()

class B(models.Model):
    a = models.ForeignKey(A, null=True, blank=True)

Now if we delete an entry of A, the cascading behavior will cause the reference in B to be deleted as well.

So far, so good. Now we want to reverse this behavior. The obvious way as people have mentioned is to use the signals emitted during delete, so we go:

def delete_reverse(sender, **kwargs):
    if kwargs['instance'].a:
        kwargs['instance'].a.delete()

post_delete.connect(delete_reverse, sender=B)

This seems to be perfect. It even works! If we delete a B entry, the corresponding A will also be deleted.

The PROBLEM is that this has a circular behavior which causes an exception: If we delete an item of A, because of the default cascading behavior (which we want to keep), the corresponding item of B will also be deleted, which will cause the delete_reverse to be called, which tries to delete an already deleted item!

The trick is, you need EXCEPTION HANDLING for proper implementation of reverse cascading:

def delete_reverse(sender, **kwargs):
    try:
        if kwargs['instance'].a:
            kwargs['instance'].a.delete()
    except:
        pass

This code will work either way. I hope it helps some folks.

Ali B
  • 491
  • 1
  • 5
  • 14
2

I don't think the feature you are looking at is an ORM or database concept. You just want to execute a callback when something is deleted.

So use the post_delete signal and add you callback handler there

from django.db.models.signals import post_delete
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(post_delete, sender=MyModel)
def my_post_delete_callback(sender, **kwargs):
    #Sender is the model which when deleted should trigger this action
    #Do stuff like delete other things you want to delete
    #The object just deleted can be accessed as kwargs[instance]
Pratik Mandrekar
  • 9,362
  • 4
  • 45
  • 65
  • @claytond While you are correct about the `delete` not being called, as quoted from documentation, I cannot find mention of the `post_delete` signal not being guaranteed to be called. In fact I just ran a simple test in django 2.0.4 and showed MyModel.delete is indeed not called, but my `pre_delete` and `post_delete` signal callbacks are called – Andy Jan 15 '19 at 16:11
  • 1
    @Andy you're right. I ran into issues with bulk `update` ("update() does an update at the SQL level and, thus, does not call any save() methods on your models, nor does it emit the pre_save or post_save signals"). I didn't realize the bulk delete was implemented differently. I'm deleting the original comment to ensure it doesn't confuse. – claytond Jan 15 '19 at 19:35