¿Cuáles son las opciones para anular el comportamiento de eliminación en cascada de Django?
django-signals cascading-deletes (3)
Django solo emula el comportamiento de CASCADE.
De acuerdo con el debate en Django Users Group, las soluciones más adecuadas son:
- Para repetir el escenario ON DELETE SET NULL, haga manualmente obj.rel_set.clear () (para cada modelo relacionado) antes de obj.delete ().
- Para repetir el escenario ON DELETE RESTRICT, compruebe manualmente que obj.rel_set está vacío antes de obj.delete ().
Los modelos Django generalmente manejan el comportamiento ON DELETE CASCADE de manera bastante adecuada (de una manera que funciona en bases de datos que no lo admiten de forma nativa).
Sin embargo, estoy luchando para descubrir cuál es la mejor manera de anular este comportamiento donde no es apropiado, en los siguientes escenarios, por ejemplo:
ON DELETE RESTRICT (es decir, evita eliminar un objeto si tiene registros secundarios)
ON DELETE SET NULL (es decir, no elimine un registro secundario, sino que establezca su clave principal en NULL para romper la relación)
Actualice otros datos relacionados cuando se borre un registro (por ejemplo, borrando un archivo de imagen cargado)
Las siguientes son las posibles formas de lograr esto de las que soy consciente:
Reemplaza el método
delete()
del modelo. Si bien este tipo de trabajo funciona, se evita cuando los registros se eliminan mediante unQuerySet
. Además, se debe anular eldelete()
cada modelo para asegurarse de que nunca se llame al código de Django y no se pueda llamar asuper()
ya que puede usar unQuerySet
para eliminar objetos secundarios.Usa señales. Esto parece ser ideal, ya que se llaman cuando se elimina directamente el modelo o se elimina a través de un QuerySet. Sin embargo, no hay posibilidad de evitar que un objeto secundario se elimine, por lo que no se puede implementar ON CASCADE RESTRICT o SET NULL.
Use un motor de base de datos que maneje esto correctamente (¿qué hace Django en este caso?)
Espere hasta que Django lo soporte (y viva con errores hasta entonces ...)
Parece que la primera opción es la única viable, pero es fea, arroja al bebé con el agua del baño y se arriesga a perder algo cuando se agrega una nueva modelo / relación.
¿Me estoy perdiendo de algo? ¿Alguna recomendación?
Ok, la siguiente es la solución en la que me he decidido, aunque está lejos de ser satisfactoria.
He agregado una clase base abstracta para todos mis modelos:
class MyModel(models.Model):
class Meta:
abstract = True
def pre_delete_handler(self):
pass
Un manejador de señal pre_delete
cualquier evento pre_delete
para las subclases de este modelo:
def pre_delete_handler(sender, instance, **kwargs):
if isinstance(instance, MyModel):
instance.pre_delete_handler()
models.signals.pre_delete.connect(pre_delete_handler)
En cada uno de mis modelos, simulo cualquier relación " ON DELETE RESTRICT
" arrojando una excepción desde el método pre_delete_handler
si existe un registro secundario.
class RelatedRecordsExist(Exception): pass
class SomeModel(MyModel):
...
def pre_delete_handler(self):
if children.count():
raise RelatedRecordsExist("SomeModel has child records!")
Esto aborta la eliminación antes de que se modifiquen los datos.
Desafortunadamente, no es posible actualizar ningún dato en la señal pre_delete (por ejemplo, para emular ON DELETE SET NULL
) ya que Django ya ha generado la lista de objetos para eliminar antes de enviar las señales. Django hace esto para evitar quedarse atascado en referencias circulares y para evitar señalar un objeto varias veces innecesariamente.
Garantizar que se pueda realizar una eliminación ahora es responsabilidad del código de llamada. Para ayudar con esto, cada modelo tiene un método prepare_delete()
que se encarga de establecer las claves en NULL
través de self.related_set.clear()
o similar:
class MyModel(models.Model):
...
def prepare_delete(self):
pass
Para evitar tener que cambiar demasiado código en mi views.py
y models.py
, el método delete()
se MyModel
en MyModel
para llamar a prepare_delete()
:
class MyModel(models.Model):
...
def delete(self):
self.prepare_delete()
super(MyModel, self).delete()
Esto significa que cualquier eliminación explícitamente llamada vía obj.delete()
funcionará como se esperaba, pero si una eliminación se ha realizado en cascada desde un objeto relacionado o se realiza mediante un queryset.delete()
y el código de llamada no ha asegurado que todos los enlaces sean roto donde sea necesario, entonces el pre_delete_handler
lanzará una excepción.
Y, por último, he agregado un método post_delete_handler
similar a los modelos a los que se llama en la señal post_delete
y permite que el modelo elimine cualquier otro dato (por ejemplo, eliminar archivos para ImageField
).
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)
Espero que eso ayude a alguien y que el código se pueda volver a enhebrar para convertirlo en algo más utilizable sin demasiados problemas.
Cualquier sugerencia sobre cómo mejorar esto es más que bienvenida.
Solo una nota para aquellos que se encuentran con este problema, ahora hay una solución incorporada en Django 1.3.
Consulte los detalles en la documentación django.db.models.ForeignKey.on_delete Gracias al editor del sitio Fragments of Code por señalarlo.
El escenario más simple posible solo agrega en tu definición de campo FK modelo:
on_delete=models.SET_NULL