Makina Blog

Le blog Makina-corpus

Vali­der les types énumé­rés Django en base de données


Appre­nez à mettre en place une contrainte SQL permet­tant de vali­der les types énumé­rés en base de données à l’aide de l’ORM Django.

Dans cet article, vous appre­nez à assu­rer l’in­té­grité de vos données en utili­sant les fonc­tion­na­li­tés natives de Post­greSQL. À l’aide de Django, nous allons mettre en place une contrainte SQL. Cette contrainte nous sert à vali­der que les valeurs d’une colonne font bien partie des valeurs auto­ri­sées pour notre type énuméré.

À la fin de cet article, vous aurez donc appris à assu­rer l’in­té­grité de vos données dans votre base quand vous utili­sez des types énumé­rés de Django.

Intro­duc­tion

Aujour­d’hui dans Django, quand on souhaite créer un type énuméré nous utili­sons un TextField avec l’at­tri­but choices.

Par exemple, si on a modé­lisé un proces­sus métier par une machine à état et que celle-ci a cinq états :

  • à faire
  • en cours de trai­te­ment
  • en test
  • terminé
  • livré

Alors on peut enre­gis­trer cette infor­ma­tion d’état en base de données en utili­sant une colonne de texte. On enre­gis­trera dans cette colonne de texte l’une des cinq valeurs repré­sen­tant notre état : toute autre valeur n’a pas de sens par rapport à la modé­li­sa­tion.

Voici un exemple de modèle Django implé­men­tant cette situa­tion :

# mon_app/models.py
from django.db import models


class EtatProcess(models.TextChoices):
    A_FAIRE = "A FAIRE", "À faire"
    EN_COURS = "EN COURS", "En cours de traitement"
    EN_TEST = "EN TEST", "En test"
    TERMINE = "TERMINE", "Terminé"
    LIVRE = "LIVRE", "Livré"


class Process(models.Model):
    etat = models.TextField(choices=EtatProcess.choices)

On retrouve deux classes :

  • La classe EtatProcess hérite de models.TextChoices et permet de défi­nir les valeurs de notre énumé­ra­tion
  • La classe Process défi­nit un modèle Django avec un champ état de type TextField.

Comme j’ai spéci­fié l’ar­gu­ment choices sur le champ etat, Django va acti­ver les compor­te­ments suivants :

  • Mise en place d’un widget spéci­fique sur les formu­laires : au lieu d’avoir un champ de texte libre nous aurons un sélec­teur
  • Conver­sion auto­ma­tique entre la valeur stockée en base de données (A FAIRE dans le cas de notre exemple), et la valeur affi­chée dans notre inter­face (À faire)
  • Vali­da­tion au niveau du champ pour s’as­su­rer que la valeur saisie fait bien partie des cinq valeurs possibles

Là où le frame­work trouve ses limites, c’est que la vali­da­tion du modèle ne s’exé­cute pas si on n’uti­lise pas ModelForm/ModelSerializer ou que l’on oublie d’ap­pe­ler Model.full_clean.

Donc dans les cas suivants, vous n’avez pas de vali­da­tion :

  1. Vous utili­sez Model.bulk_create ou Model.bulk_update qui n’ap­pellent pas Model.full_clean() et qui ne font donc pas de vali­da­tion
  2. Vous utili­sez QuerySet.update qui n’ap­pelle pas Model.full_clean()
  3. Vous appe­lez la méthode Model.save() sans avoir appelé Model.full_clean()
  4. On met à jour la donnée direc­te­ment en base, sans passer par Django

Donc, si nous n’uti­li­sons pas ModelForm/ModelSerializer il y a pas mal de trous dans la raquette en termes de vali­da­tion !

Pour se proté­ger contre des valeurs inva­lides dans la colonne etat, mettre en place une contrainte SQL est inté­res­sant qu’il faut véri­fier côté SGBD (système de gestion de base de données).

Et Django peut nous aider pour ça ! On va pouvoir utili­ser le système de migra­tion pour instal­ler/désins­tal­ler la contrainte et nous allons utili­ser l’ORM pour expri­mer celle-ci.

Instal­la­tion de la contrainte

La décla­ra­tion d’une contrainte SQL se fait via l’at­tri­but constraints de la classe interne Meta de votre modèle :

# mon_app/models.py
from django.db import models

# ...

class Process(models.Model) :
    etat = models.TextField(choices=EtatProcess.choices)

    class Meta :
        constraints = []

On peut instan­cier deux types d’objets dans constraints :

  1. Des instances de CheckConstraint
  2. Des instances de UniqueConstraint

Nous devons défi­nir l’at­tri­but name de notre contrainte à l’ins­tan­cia­tion et celui-ci doit être unique.

À chaque modi­fi­ca­tion de l’at­tri­but constraints, il est néces­saire de refaire un makemigrations.

Écri­ture de la condi­tion de la contrainte

Lorsque l’on instan­cie une CheckConstraint, on doit spéci­fier la condi­tion qu’il faut véri­fier côté SGBD. Pour cela, on peut passer deux types d’objets : des objets Q ou des objets Expression.

Les objets Q servent à repré­sen­ter des condi­tions SQL. On s’en sert souvent quand on a besoin de réali­ser des ou logiques entre des condi­tions. Ici nous allons écrire la condi­tion qui véri­fie que la valeur du champ fait partie de la liste des valeurs possibles.

# mon_app/models.py
from django.db import models, Q

# ...


class Process(models.Model):
    etat = models.TextField(choices=EtatProcess.choices)

    class Meta:
        constraints = [
            models.CheckConstraint(
                condition=Q(etat__in=EtatProcess.values)
            )
        ]

On défi­nit ici une contrainte SQL dont l’ex­pres­sion vaut vrai si la valeur de la colonne état est parmi les valeurs de l’enum EtatProcess. Il ne nous reste plus qu’à donner un nom à notre contrainte et à ajou­ter un message d’er­reur utile pour l’uti­li­sa­teur. L’im­plé­men­ta­tion finale est la suivante :

# mon_app/models.py
from django.db import models, Q


class EtatProcess(models.TextChoices):
    A_FAIRE = "A FAIRE", "À faire"
    EN_COURS = "EN COURS", "En cours de traitement"
    EN_TEST = "EN TEST", "En test"
    TERMINE = "TERMINE", "Terminé"
    LIVRE = "LIVRE", "Livré"


class Process(models.Model):
    etat = models.TextField(choices=EtatProcess.choices)

    class Meta:
        constraints = [
            models.CheckConstraint(
                condition=Q(etat__in=EtatProcess.values),
                name="%(app_label)s_%(class)s_etat_enum",
                violation_error_message=f"La valeur de l'attribut 'etat' est invalide."
                f"Les valeurs possibles sont {','.join(EtatProcess.values)}.",
            )
        ]

L’at­tri­but violation_error_message défi­nit le message d’er­reur qui est remonté dans les inter­faces (admin, form, etc.) utili­sa­teurs. Je vous recom­mande de l’uti­li­ser de manière systé­ma­tique car le message géné­rique de Django est peu parlant.

Dans cet exemple, le nom de la contrainte est dérivé du nom de l’ap­pli­ca­tion Django et du nom de la classe. Le message d’er­reur est fabriqué à partir de l’enum pour inclure la liste des valeurs possibles.

Pour mettre en place la contrainte il ne reste plus qu’à jouer les migra­tions : makemigrations puis migrate.

Chaque modi­fi­ca­tion de la classe EtatProcess (ajout, suppres­sion ou modi­fi­ca­tion de valeurs) entraî­nera la modi­fi­ca­tion de la contrainte SQL de manière auto­ma­tique.

Comme la contrainte est instal­lée en base de données, nous sommes donc assu­rés que la colonne etat ne contien­dra que des valeurs issues de l’enum EtatProcess !

Pour finir, quelques limites de ce système de véri­fi­ca­tion :

  1. On ne peut pas expri­mer de contraintes multi-tables avec les CHECK CONSTRAINT.
  2. La contrainte doit être immuable (immu­table dans la termi­no­lo­gie Post­greSQL). Si vous faites appel à une fonc­tion SQL que vous avez écrite dans votre contrainte, et que vous modi­fiez votre fonc­tion vous cassez votre base. En effet Post­greSQL n’exé­cute pas la contrainte de véri­fi­ca­tion sur les lignes exis­tantes dans votre table, unique­ment sur les INSERT/UPDATE.

Formations associées

Formations Django

Formation Django initiation

Nantes Du 11 au 13 mars 2025

Voir la Formation Django initiation

Formations Django

Formation Django avancé

À distance (FOAD) Du 17 au 21 mars 2025

Voir la Formation Django avancé

Formations Django

Formation Django REST Framework

À distance (FOAD) Du 9 au 13 juin 2025

Voir la Formation Django REST Framework

Actualités en lien

Makina Corpus est spon­sor de la PyConFR 2024

21/10/2024

Le soutien de Makina Corpus à la PyConFR 2024, qui se tient du 31 octobre au 3 novembre 2024 à Stras­bourg, reflète ses valeurs de partage et d’in­no­va­tion, et son enga­­ge­­ment envers la commu­nauté dyna­­mique et ouverte de Python.

Voir l'article
Image
Encart PyConFr 2024

Revoir les webi­naires : décou­verte de l’ou­til CANARI-France

10/04/2024

L’ap­pli­ca­tion CANARI-France est destiné aux acteurs agri­coles afin de calcu­ler des indi­ca­teurs agro-clima­tiques à partir de projec­tions clima­tiques. Décou­vrer en le replay des 4 webi­naires orga­ni­sés par Sola­gro et l’ADEME.

Voir l'article
Image
Webinaire découverte de Canari

Utiliser des fonctions PostgreSQL dans des contraintes Django

07/11/2023

Cet article vous présente comment utiliser les fonctions et les check constraints PostgreSQL en tant que contrainte sur vos modèles Django.

Voir l'article
Image
Django PostgreSQL

Inscription à la newsletter

Nous vous avons convaincus