Accueil / Blog / Métier / 2013 / Django-Safedelete

Django-Safedelete

Par stkau — publié 09/07/2013
Masquage d'objets en base de données (alternative à la suppression définitive).

Je vais vous parler d'une application que j'ai développée dans le cadre de mon stage à Makina Corpus.

Présentation

Un besoin récurrent pour beaucoup de Djangonautes est d'avoir la possibilité de masquer des objets avec l'ORM Django, au lieu de les supprimer définitivement. C'est pour répondre à ce besoin que l'application django-safedelete a été développée.

Dans cette optique de masquage, des comportements différents peuvent être nécessaires selon les situations :
  • On peut vouloir simuler le comportement habituel de suppression de données, en masquant les objets supprimés. Ainsi ils ne seront plus accessibles lors de futures requêtes, comme cela aurait été le cas s'ils avaient été réellement supprimés.
  • Dans le cas d'une suppression en cascade, il est possible de masquer - au lieu de supprimer - les objets concernés par la cascade. Les objets qui en dépendent pourront toujours pointer vers lui.
  • Il est aussi possible de supprimer un élément, sauf si d'autres en dépendent, auquel cas il sera simplement masqué. Pratique pour un site de e-commerce, où la suppression d'articles provoquerait juste leur masquage pour éviter de nouveaux achats, en permettant toujours d'accéder à ceux-ci s'ils sont référencés dans des commandes.

Bien entendu, lors de la suppression on peut forcer un comportement. Quand on récupère une liste d'objets, on peut aussi choisir de récupérer aussi les articles masqués.

L'application est disponible sur github et pypi.

Elle expose une « factory », capable de générer des mixins, dont vos modèles django doivent hériter. Un exemple est disponible dans le README, et une documentation est en ligne.

La technique

Cette application est relativement similaire à django-logicaldelete, mais ce projet ne propose pas de choisir entre plusieurs comportements (pas de masquage en cascade notamment). Le projet MozTrap (qui sert de base aux projets de Mozilla) possède un modèle de base qui intègre également cette fonction, et va plus loin en stockant des dates et d'autres informations générales. Mais tout comme logicaldelete, il n'est pas générique puisqu'il ne propose qu'un seul comportement fixé.

Le mixin que l'application fournit contient un champ booléen deleted, qui indique si l'objet a été masqué ou non. Il surcharge également la méthode delete() en supprimant ou en masquant l'objet, selon le comportement souhaité.

La base est donc plutôt simple. Mais creusons un petit peu…

On se rend compte qu'on va devoir aussi remplacer la méthode delete() des QuerySet que renvoie notre manager, puisque par défaut elle va supprimer tous les objets de la base de données. En plus de définir notre propre manager, il nous faut donc aussi définir nos propres querysets.

Problème : si on utilise aussi des applications comme GeoDjango, qui utilisent leur propre type de managers/querysets, cela devient plus complexe à résoudre.

Pour pallier ce problème, les managers/queryset sont créés à la volée, en les faisant hériter d'une superclasse passée en paramètre. Du coup, en passant les bonnes classes à la fonction qui crée le mixin, le problème est contourné.

Les ennuis commencent

Si on a des relations entre objets (ForeignKey, ManyToMany, ...), comment doit-on réagir ?

Le comportement de Django est plutôt cohérent, mais n'est pas du tout documenté à l'heure ou ces lignes sont écrites :
  • Dans le cas de relations pointant vers un objet simple, Django va utiliser le manager « de base » (_base_manager), pour récupérer le QuerySet d'où il extraira l'objet.
  • Pour une relation « inverse », où on récupère un ensemble d'objets liés (reverse-ForeignKey ou ManyToManyField), Django utilisera le manager par défaut (_default_manager).

Dans notre cas, les relations qui pointent sur un unique objet resteront donc fonctionnelles, même si l'objet a été masqué. Pour les relations qui renvoient une liste d'objets, on passera bien par notre manager qui masquera les objets devant l'être.

On peut aussi vouloir rendre vraiment inaccessibles les objets masqués, et c'est là que ça se complique.

Il est possible, en définissant une variable de classe use_for_related_fields du manager par défaut valant True, de forcer Django à utiliser le manager par défaut pour toutes les relations : en interne, il va alors mettre _base_manager à la valeur de _default_manager.

Cela pourrait donc permettre de « casser » les relations qui pointent vers un unique objet si ce dernier a été supprimé.

Malheureusement, utiliser use_for_related_fields sur un manager qui masque des objets est une très mauvaise idée : En interne, le _base_manager est très utilisé, et Django sera amené à croire que des objets masqués n'existent pas dans la BDD. Si on les sauve, il effectuera donc un INSERT au lieu d'un UPDATE

Tentative de résolution

La gestion des managers pour les champs « liés » dans Django s'avère donc cohérente, mais non documentée et très peu flexible. Pas mal de bugs ont été ouverts sur le sujet, et une discussion a été entamée sur la mailing-list des développeurs de Django.

Parmi les suggestions proposées, une solution a été implémentée afin de rajouter de la flexibilité au système. Le principe est de définir un champ ForeignKey, ManyToManyField… dans notre modèle, en spécifiant deux paramètres optionnels manager_class et reverse_manager_class, afin de nous permettre d'utiliser des managers spécifiques.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Python : Bien configurer son environnement de développement Python : Bien configurer son environnement de développement 07/12/2015

Comment utiliser les bonnes pratiques de développement Python.

Formation Django initiation à Toulouse du 13 au 15 mars Formation Django initiation à Toulouse du 13 au 15 mars 26/01/2017

Entrez de plain-pied dans l'univers de Django aux côtés de développeurs ayant une expérience de ...

Retour sur la PyConFr 2016 Retour sur la PyConFr 2016 18/10/2016

Nous étions présents à Rennes pour PyConFr 2016. Voici notre compte-rendu à chaud.

Wagtail: How to use the Page model and its manager (part 2) Wagtail: How to use the Page model and its manager (part 2) 08/08/2016

The Page model has several methods specific to Wagtail. This is also the case of its manager. We ...

Wagtail : How to make your own content type models (part 1) Wagtail : How to make your own content type models (part 1) 29/07/2016

We are used to initialize our CMS directly from a web interface, often including lots of complex ...