web-dev-qa-db-fra.com

Quelles sont les options pour remplacer le comportement de suppression en cascade de Django?

Les modèles Django gèrent généralement le comportement ON DELETE CASCADE de manière assez adéquate (d'une manière qui fonctionne sur les bases de données qui ne le supportent pas nativement.)

Cependant, j'ai du mal à découvrir quelle est la meilleure façon de remplacer ce comportement là où il n'est pas approprié, dans les scénarios suivants par exemple:

  • ON DELETE RESTRICT (c'est-à-dire empêcher la suppression d'un objet s'il a des enregistrements enfants)

  • ON DELETE SET NULL (c'est-à-dire ne supprimez pas un enregistrement enfant, mais définissez sa clé parent sur NULL à la place pour rompre la relation)

  • Mettre à jour d'autres données connexes lorsqu'un enregistrement est supprimé (par exemple, supprimer un fichier image téléchargé)

Voici, à ma connaissance, les moyens potentiels d'y parvenir:

  • Remplacez la méthode delete() du modèle. Bien que ce genre de travaux, il est contourné lorsque les enregistrements sont supprimés via un QuerySet. De plus, la fonction delete() de chaque modèle doit être remplacée pour s'assurer que le code de Django n'est jamais appelé et que super() ne peut pas être appelé car il peut utiliser un QuerySet pour supprimer les objets enfants .

  • Utilisez des signaux. Cela semble idéal car ils sont appelés lors de la suppression directe du modèle ou de la suppression via un QuerySet. Cependant, il n'y a aucune possibilité d'empêcher un objet enfant d'être supprimé, il n'est donc pas utilisable d'implémenter ON CASCADE RESTRICT ou SET NULL.

  • Utilisez un moteur de base de données qui gère cela correctement (que fait Django dans ce cas?)

  • Attendez que Django le supporte (et vivez avec des bugs jusque-là ...)

Il semble que la première option soit la seule viable, mais elle est laide, jette le bébé avec l'eau du bain et risque de manquer quelque chose lorsqu'un nouveau modèle/relation est ajouté.

Suis-je en train de manquer quelque chose? Des recommandations?

57
Tom

Juste une note pour ceux qui rencontrent également ce problème, il existe maintenant une solution intégrée dans Django 1.3.

Voir les détails dans la documentation Django.db.models.ForeignKey.on_delete Merci pour l'éditeur du site Fragments of Code de le signaler.

Le scénario le plus simple possible vient d'ajouter dans votre définition de champ FK de modèle:

on_delete=models.SET_NULL
79
blasio

Django émule uniquement le comportement CASCADE.

Selon discussion in Django Users Group, les solutions les plus adéquates sont:

  • Pour répéter le scénario ON DELETE SET NULL - faites manuellement obj.rel_set.clear () (pour chaque modèle connexe) avant obj.delete ().
  • Pour répéter le scénario ON DELETE RESTRICT - vérifiez manuellement si obj.rel_set est vide avant obj.delete ().
6
Leonid Shvechikov

D'accord, voici la solution sur laquelle je me suis fixé, bien qu'elle soit loin d'être satisfaisante.

J'ai ajouté une classe de base abstraite pour tous mes modèles:

class MyModel(models.Model):
    class Meta:
        abstract = True

    def pre_delete_handler(self):
        pass

Un gestionnaire de signaux intercepte tous les événements pre_delete Pour les sous-classes de ce modèle:

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

Dans chacun de mes modèles, je simule toutes les relations "ON DELETE RESTRICT" En lançant une exception de la méthode pre_delete_handler Si un enregistrement enfant existe.

class RelatedRecordsExist(Exception): pass

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

Cela annule la suppression avant toute modification des données.

Malheureusement, il n'est pas possible de mettre à jour des données dans le signal pre_delete (par exemple pour émuler ON DELETE SET NULL) Car la liste des objets à supprimer a déjà été générée par Django avant les signaux sont envoyés. Django fait cela pour éviter de rester bloqué sur des références circulaires et pour éviter de signaler un objet plusieurs fois inutilement.

Veiller à ce qu'une suppression puisse être effectuée incombe désormais au code appelant. Pour vous aider, chaque modèle a une méthode prepare_delete() qui prend soin de définir les clés sur NULL via self.related_set.clear() ou similaire:

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

Pour éviter d'avoir à changer trop de code dans mes views.py Et models.py, La méthode delete() est surchargée sur MyModel pour appeler prepare_delete():

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

Cela signifie que toutes les suppressions explicitement appelées via obj.delete() fonctionneront comme prévu, mais si une suppression est en cascade à partir d'un objet lié ou est effectuée via une queryset.delete() et que le code appelant n'est pas assuré que tous les liens sont rompus si nécessaire, alors pre_delete_handler lèvera une exception.

Et enfin, j'ai ajouté une méthode post_delete_handler Similaire aux modèles qui est appelée sur le signal post_delete Et permet au modèle d'effacer toutes les autres données (par exemple, supprimer des fichiers pour ImageFields.)

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)

J'espère que cela aide quelqu'un et que le code peut être re-enfilé en quelque chose de plus utilisable sans trop de problèmes.

Toute suggestion sur la façon d'améliorer cela est plus que bienvenue.

4
Tom