What is the reverse equivalent of cascade?
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 Song
s "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?
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)
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.
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.
Django 1.3 will provide the ability to customise cascade behaviour [1], but as far as I'm aware there still won't be a way to achieve what you're describing automatically.
Probably the best method would be to write a post_delete
signal handler [2] for your Song
class, which checks whether the related Album
has any Song
s remaining and deletes it if appropriate. I believe the ForeignKey
value should still be there in the instance
argument, despite the Song
having been deleted from the database. (If not, try using pre_delete
)
Alternatively you could override the delete
method[3], but this is not always called depending on how you delete your objects.
[1] http://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.on_delete
[2] http://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_delete
[3] http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.delete
精彩评论