Django models generally handle the ON DELETE CASCADE behaviour quite adequately (in a way that works on databases that don't support it natively.)
However, I'm struggling to discover what is the best way to override this behaviour where it is not appropriate, in the following scenarios for example:
ON DELETE RESTRICT (i.e. prevent deleting an object if it has child records)
ON DELETE SET NULL (i.e. don't delete a child record, but set it's parent key to NULL instead to break the relationship)
Update other related data when a record is deleted (e.g. deleting an uploaded image file)
The following are the potential ways to achieve these that I am aware of:
Override the model's
delete()
method. 开发者_如何学编程While this sort of works, it is sidestepped when the records are deleted via aQuerySet
. Also, every model'sdelete()
must be overridden to make sure Django's code is never called andsuper()
can't be called as it may use aQuerySet
to delete child objects.Use signals. This seems to be ideal as they are called when directly deleting the model or deleting via a QuerySet. However, there is no possibility to prevent a child object from being deleted so it is not usable to implement ON CASCADE RESTRICT or SET NULL.
Use a database engine that handles this properly (what does Django do in this case?)
Wait until Django supports it (and live with bugs until then...)
It seems like the first option is the only viable one, but it's ugly, throws the baby out with the bath water, and risks missing something when a new model/relation is added.
Am I missing something? Any recommendations?
Just a note for those who run into this issue as well, there is now an built-in solution in Django 1.3.
See the details in the documentation django.db.models.ForeignKey.on_delete Thanks for editor of Fragments of Code site to point it out.
The simplest possible scenario just add in your model FK field definition:
on_delete=models.SET_NULL
Django only emulates CASCADE behaviour.
According to discussion in Django Users Group the most adequate solutions are:
- To repeat ON DELETE SET NULL scenario - manually do obj.rel_set.clear() (for every related model) before obj.delete().
- To repeat ON DELETE RESTRICT scenario - manually check is obj.rel_set empty before obj.delete().
Ok, the following is the solution I've settled on, though it's far from satisfying.
I've added an abstract base class for all my models:
class MyModel(models.Model):
class Meta:
abstract = True
def pre_delete_handler(self):
pass
A signal handler catches 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
" relations by throwing an exception from the pre_delete_handler
method if a child record exists.
class RelatedRecordsExist(Exception): pass
class SomeModel(MyModel):
...
def pre_delete_handler(self):
if children.count():
raise RelatedRecordsExist("SomeModel has child records!")
This aborts the delete before any data is modified.
Unfortunately, it is not possible to update any data in the pre_delete signal (e.g. to emulate ON DELETE SET NULL
) as the list of objects to delete has already been generated by Django before the signals are sent. Django does this to avoid getting stuck on circular references and to prevent signaling an object multiple times unnecessarily.
Ensuring a delete can be performed is now the responsibility of the calling code. To assist with this, each model has a prepare_delete()
method that takes care of setting keys to NULL
via self.related_set.clear()
or similar:
class MyModel(models.Model):
...
def prepare_delete(self):
pass
To avoid having to change too much code in my views.py
and models.py
, the delete()
method is overridden on MyModel
to call prepare_delete()
:
class MyModel(models.Model):
...
def delete(self):
self.prepare_delete()
super(MyModel, self).delete()
This means that any deletes explicitly called via obj.delete()
will work as expected, but if a delete has cascaded from a related object or is done via a queryset.delete()
and the calling code hasn't ensured that all links are broken where necessary, then the pre_delete_handler
will throw an exception.
And lastly, I've added a similar post_delete_handler
method to the models that gets called on the post_delete
signal and lets the model clear up 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 that helps someone and that the code can be re-threaded back into something more useable without too much trouble.
Any suggestions on how to improve this are more than welcome.
精彩评论