21 votes

Django DRY Validation du modèle, du formulaire et du sérialiseur

J'ai quelques difficultés à trouver le meilleur endroit (lire : DRY et maintenable) pour introduire une logique de validation dans Django, notamment entre les modèles, les formulaires et les sérialiseurs DRF.

Je travaille avec Django depuis plusieurs années et j'ai suivi les différentes conventions pour gérer la validation des modèles, des formulaires et des points de terminaison de l'API REST. J'ai essayé de nombreuses variantes pour assurer l'intégrité globale des données, mais j'ai récemment rencontré une petite pierre d'achoppement. Voici une brève liste de ce que j'ai essayé après avoir parcouru de nombreux articles, messages de l'OS et tickets :

  1. Validation au niveau du modèle ; à savoir, s'assurer que toutes mes contraintes personnalisées correspondent avant d'appeler myModel.save() en passant outre myModel.clean() (ainsi que des méthodes communes spécifiques au domaine et uniques). Pour ce faire, j'ai veillé à myModel.full_clean() a été appelé en myForm.clean() (pour les formulaires -- et le panneau d'administration le fait déjà) et mySerializer.validate() (pour les sérialiseurs DRF).

  2. Validation au niveau du formulaire et du sérialiseur, en faisant appel à une méthode partagée pour un code maintenable et DRY.

  3. Validation au niveau du formulaire et du sérialiseur, avec une méthode distincte pour chacun d'eux afin de garantir une flexibilité maximale (par exemple, lorsque les formulaires et les terminaux ont des contraintes différentes).

La première méthode me semble la plus intuitive lorsque les formulaires et les sérialiseurs ont des contraintes identiques, mais elle est un peu désordonnée en pratique ; d'abord, les données sont automatiquement nettoyées et validées par le formulaire ou le sérialiseur, puis l'entité du modèle est instanciée, et une nouvelle validation est exécutée - ce qui est un peu alambiqué et peut devenir compliqué.

La troisième méthode est celle que Django Rest Framework recommande à partir de la version 3.0 ; ils ont éliminé une grande partie de leur système de gestion de l'information. model.save() et préfèrent laisser la validation aux aspects de l'application destinés aux utilisateurs. Cela me semble logique, puisque la base de Django model.save() n'appelle pas model.full_clean() de toute façon.

Ainsi, la méthode deux me semble être le meilleur résultat global généralisé ; la validation vit dans un endroit distinct -- avant que le modèle ne soit jamais touché -- et la base de code est moins encombrée / plus DRY en raison de la logique de validation partagée.

Malheureusement, la plupart des problèmes que j'ai rencontrés sont liés à la coopération des sérialiseurs de Django Rest Framework. Les trois approches fonctionnent bien pour les formulaires, et en fait pour la plupart des méthodes HTTP (notamment lors de la création d'une entité par POST), mais aucune ne semble fonctionner correctement lors de la mise à jour d'une entité existante (PUT, PATCH).

Pour faire court, il s'est avéré assez difficile de valider les données entrantes lorsqu'elles sont incomplètes (mais par ailleurs valides - c'est souvent le cas pour PATCH). Les données de la demande peuvent ne contenir que certains champs -- ceux qui contiennent des informations différentes/nouvelles -- et les informations existantes de l'instance du modèle sont maintenues pour tous les autres champs. En fait, Numéro 4306 de la DRF résume parfaitement ce défi particulier.

J'ai également envisagé d'exécuter la validation du modèle personnalisé au niveau du viewset (après le remplissage de serializer.validated_data et l'existence de serializer.instance, mais avant l'appel de serializer.save()), mais j'ai encore du mal à trouver une approche propre et généralisée en raison de la complexité du traitement des mises à jour.

TL;DR Le cadre Django Rest rend un peu difficile l'écriture d'une logique de validation propre et maintenable à un endroit évident, en particulier pour les mises à jour partielles qui reposent sur un mélange de données de modèle existantes et de données de requête entrantes.

J'aimerais que des gourous de Django nous disent ce qu'ils ont réussi à faire, car je ne vois pas de solution pratique.

Gracias.

6voto

SamuelMS Points 171

Je viens de réaliser que je n'ai jamais posté ma solution en réponse à cette question. J'ai fini par écrire un mixin de modèle pour toujours exécuter la validation avant de sauvegarder ; c'est un peu gênant car la validation sera techniquement exécutée deux fois dans les formulaires de Django (c'est-à-dire dans le panneau d'administration), mais cela me permet de garantie que la validation est exécutée - indépendamment de ce qui déclenche une sauvegarde du modèle. Je n'utilise généralement pas les formulaires de Django, donc cela n'a pas beaucoup d'impact sur mes applications.

Voici un extrait rapide qui fait l'affaire :

class ValidatesOnSaveModelMixin:
    """ ValidatesOnSaveModelMixin
    A mixin that ensures valid model state prior to saving.
    """
    def save(self, **kwargs):
        self.full_clean()
        super(ValidatesOnSaveModelMixin, self).save(**kwargs)

Voici comment l'utiliser :

class ImportantModel(ValidatesOnSaveModelMixin, models.Model):
    """ Will always ensure its fields pass validation prior to saving. """

Il y a une mise en garde importante : toutes les opérations de Django allant directement à la base de données (c.-à-d. ImportantModel.objects.update() ) ne font pas appel à la fonction save() et ne seront donc pas validés. Il n'y a pas grand-chose à faire à ce sujet, puisque ces méthodes ont pour but d'optimiser les performances en évitant un certain nombre d'appels à la base de données - soyez donc conscient de leur impact si vous les utilisez.

4voto

jmoz Points 984

Je suis d'accord, le lien entre modèles/serialiseurs/validation est rompu.

La meilleure solution DRY que j'ai trouvée est de garder la validation dans le modèle, avec des validateurs spécifiés sur les champs, puis, si nécessaire, la validation au niveau du modèle dans clean() écrasé.

Ensuite, dans le sérialiseur, surchargez la validation et appelez le modèle. clean() Par exemple, dans MySerializer :

def validate(self, data):
    instance = FooModel(**data)
    instance.clean()
    return data

Ce n'est pas très joli, mais je préfère cela à la validation à deux niveaux dans le sérialiseur et le modèle.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X