Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the reverse equivalent of cascade?

Tags:

python

django

I am writing a small music database. Learning SQL lies quite a long time in my past and I always wanted to give Django a try. But there is one thing I couldn't wrap my head around.

Right now, my models only consist of two Classes, Album and Song. Song has a foreign key pointing to the album it belongs to. Now if I would delete that Album, all Songs "belonging" to it, would be deleted due to the cascading effect.

Albums are kinda virtual in my database, only songs are actually represented on the filesystem and the albums are constructed according to the songs tags, therefore I can only know an album doesn't exist anymore if there are no more songs pointing to it (as they no longer exist in the filesystem).

Or in short, how can I achieve a cascade in reverse, that means, if no more songs are pointing to an album, the album should be deleted as well?

like image 823
user422039 Avatar asked Mar 03 '11 19:03

user422039


3 Answers

You could use the pre_delete signal to remove the Album when a Song is deleted and there are no more songs.

from yourapp.models import Album, Song
from django.db.models.signals import pre_delete

def delete_parent(sender, **kwargs):
    # Here you check if there are remaining songs.
    ....

pre_delete.connect(delete_parent, sender=Song)
like image 166
Fábio Diniz Avatar answered Sep 23 '22 14:09

Fábio Diniz


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.

like image 23
Ali B Avatar answered Sep 22 '22 14:09

Ali B


I've had a similar problem and I've ended up adding a counter into the Album equivalent. If the count is 0 and the operation is delete(), then the album object is delete()d.

Other solution os to overload the delete() method in the song model, or use post-delete to delete the album.

like image 32
Laur Ivan Avatar answered Sep 21 '22 14:09

Laur Ivan