What are the options for overriding Django cascading delete behavior?

Django models usually handle the behavior of ON DELETE CASCADE quite adequately (so that this works with databases that do not support it natively).

However, I am struggling to find what is the best way to override this behavior when it does not fit in the following scenarios, for example:

  • ON DELETE RESTRICT (i.e. delete an object if it has children)

  • ON DELETE SET NULL (i.e. do not delete the child entry, but instead set the parent key to NULL to break the connection)

  • Updating other related data when deleting a record (for example, deleting a downloaded image file)

The following are possible ways to achieve these goals that I know of:

  • Cancel the delete() model method. Despite the fact that this type of work, it bypasses when records are deleted through QuerySet . In addition, each delete() model must be redefined to ensure that Django code is never called and super() cannot be called, since it can use a QuerySet to delete child objects.

  • Use signals. This seems ideal, as they are called when the model is deleted directly or deleted through the QuerySet. However, it is not possible to prevent the deletion of a child object, therefore it cannot be used to turn CASCADE ON OR INSTALLED ZERO.

  • Use a database engine that handles this correctly (what does Django do in this case?)

  • Wait for Django to support it (and live with bugs until then ...)

It seems that the first option is the only viable one, but it is ugly, it throws out a child with bath water and runs the risk of losing something when adding a new model / relationship.

Am I missing something? Any recommendations?

+49
django cascading-deletes django-signals
Mar 19 '10 at 5:56
source share
3 answers

Just a note for those facing this problem, there is now a built-in solution in Django 1.3.

For more information, see the documentation django.db.models.ForeignKey.on_delete Thanks for the Fragments of Code site editor to specify it.

The simplest possible scenario simply adds the definition of the FK model to your definition:

 on_delete=models.SET_NULL 
+58
Dec 08 2018-10-12T00:
source share

Django only emulates CASCADE behavior.

According to the discussion in the Django Users Group, the most appropriate solutions are:

  • Repeat the ON DELETE SET NULL script - manually execute obj.rel_set.clear () (for each associated model) until obj.delete ().
  • Repeat ON DELETE RESTRICT script - manually check obj.rel_set empty before obj.delete ().
+5
Mar 19 '10 at 6:35
source share

Well, the next solution I settled on, although not satisfying.

I have added an abstract base class for all my models:

 class MyModel(models.Model): class Meta: abstract = True def pre_delete_handler(self): pass 

The signal handler captures any pre_delete events for subclasses of this model:

 def pre_delete_handler(sender, instance, **kwargs): if isinstance(instance, MyModel): instance.pre_delete_handler() models.signals.pre_delete.connect(pre_delete_handler) 

In each of my models, I simulate any " ON DELETE RESTRICT " relationships by throwing an exception from the pre_delete_handler method if a pre_delete_handler entry exists.

 class RelatedRecordsExist(Exception): pass class SomeModel(MyModel): ... def pre_delete_handler(self): if children.count(): raise RelatedRecordsExist("SomeModel has child records!") 

This cancels the deletion before any data changes.

Unfortunately, it is not possible to update the data in the pre_delete signal (for example, to emulate ON DELETE SET NULL ), since the list of deleted objects was already created by Django before sending the signals. Django does this in order to avoid getting stuck in circular links and to prevent unnecessary signal transmission to the object several times.

Ensuring that deletion can be performed is now the responsibility of the calling code. To help with this, each model has a prepare_delete() method that takes care of setting the keys to NULL via self.related_set.clear() or similar:

 class MyModel(models.Model): ... def prepare_delete(self): pass 

To avoid too much code in my views.py and models.py , the delete() method is overridden by MyModel to call prepare_delete() :

 class MyModel(models.Model): ... def delete(self): self.prepare_delete() super(MyModel, self).delete() 

This means that any deletions explicitly called through obj.delete() will work as expected, but if the deletion was cascaded from a linked object or performed using queryset.delete() and the call code did not ensure that all links were damaged where necessary, then pre_delete_handler will throw an exception.

And finally, I added a similar post_delete_handler method to models that are called by the post_delete signal and allow the model to clear any other data (for example, deleting files for ImageField s.)

 class MyModel(models.Model): ... def post_delete_handler(self): pass def post_delete_handler(sender, instance, **kwargs): if isinstance(instance, MyModel): instance.post_delete_handler() models.signals.post_delete.connect(post_delete_handler) 

I hope this helps someone, and that the code can be re-included in something more usable without much trouble.

Any suggestions on how to improve this are more than welcome.

+4
Apr 01 '10 at 2:24
source share



All Articles