Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override django's model delete method for bulk deletion

I'm overriding Django's model delete method in order to delete orphan files in the disk for image fields, something like this:

class Image(models.Model):
    img = models.ImageField(upload_to=get_image_path)
    ...
    def delete(self, *args, **kwargs):
        self.img.delete()
        super(Image, self).delete(*args, **kwargs)

This works fine when I delete single objects from the admin, but when I select multiple objects and delete them, this doesn't seem to get called. I have been googling for a while but haven't hit the right keywords to get the answer for this, nor the official documentation seems to talk about this subject.

like image 627
Kilian Perdomo Curbelo Avatar asked Mar 06 '15 09:03

Kilian Perdomo Curbelo


People also ask

How do I override a Django model?

Whenever one tries to create an instance of a model either from admin interface or django shell, save() function is run. We can override save function before storing the data in the database to apply some constraint or fill some ready only fields like SlugField.

How do I remove all models in Django?

If you want to remove all the data from all your tables, you might want to try the command python manage.py flush . This will delete all of the data in your tables, but the tables themselves will still exist. He did say "all the data from all your tables", which should indicate that it's a very destructive operation.


3 Answers

It does:

The delete() method does a bulk delete and does not call any delete() methods on your models. It does, however, emit the pre_delete and post_delete signals for all deleted objects (including cascaded deletions).

For that to work, you can override delete method on QuerySet, and then apply that QuerySet as manager:

class ImageQuerySet(models.QuerySet):      def delete(self, *args, **kwargs):         for obj in self:             obj.img.delete()         super(ImageQuerySet, self).delete(*args, **kwargs)  class Image(models.Model):     objects = ImageQuerySet.as_manager()     img = models.ImageField(upload_to=get_image_path)     ...     def delete(self, *args, **kwargs):         self.img.delete()         super(Image, self).delete(*args, **kwargs) 
like image 174
GwynBleidD Avatar answered Sep 18 '22 04:09

GwynBleidD


Delete method of queryset works directly on the database. It does not call Model.delete() methods. From the docs:

Keep in mind that this will, whenever possible, be executed purely in SQL, and so the delete() methods of individual object instances will not necessarily be called during the process. If you’ve provided a custom delete() method on a model class and want to ensure that it is called, you will need to “manually” delete instances of that model (e.g., by iterating over a QuerySet and calling delete() on each object individually) rather than using the bulk delete() method of a QuerySet.

If you want to override Django administration interface's default behavior, you can write a custom delete action:

https://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/

Another method is to override post_delete (or pre_delete) signal instead of delete method:

https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_delete

Like pre_delete, but sent at the end of a model’s delete() method and a queryset’s delete() method.

like image 36
Selcuk Avatar answered Sep 19 '22 04:09

Selcuk


I believe this issue is addressed in the docs

where it says:

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.

As suggested in the docs above, I believe a better solution is to use the post_delete signal, like so:

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

class Image(models.Model):
    img = models.ImageField(upload_to=get_image_path)
    ...

@receiver(post_delete, sender=Image)
def delete_image_hook(sender, instance, using, **kwargs):
    instance.img.delete()

Unlike overriding the delete method, the delete_image_hook function should be called on bulk deletes and cascading deletes as well. Here is more information on using Django's Signals: https://docs.djangoproject.com/en/1.11/topics/signals/#connecting-to-signals-sent-by-specific-senders

Note on previous answers: Some of the earlier posts suggest overriding the delete method of QuerySet, which may have performance implications and other unintended behavior. Perhaps those answers were written before Django's Signals were implemented, but I think using Signals is a cleaner approach.

like image 21
modulitos Avatar answered Sep 18 '22 04:09

modulitos