Makina Blog
Valider les types énumérés Django en base de données
Dans cet article, vous apprenez à assurer l’intégrité de vos données en utilisant les fonctionnalités natives de PostgreSQL. À l’aide de Django, nous allons mettre en place une contrainte SQL. Cette contrainte nous sert à valider que les valeurs d’une colonne font bien partie des valeurs autorisées pour notre type énuméré.
À la fin de cet article, vous aurez donc appris à assurer l’intégrité de vos données dans votre base quand vous utilisez des types énumérés de Django.
Introduction
Aujourd’hui dans Django, quand on souhaite créer un type énuméré nous utilisons un TextField
avec l’attribut choices
.
Par exemple, si on a modélisé un processus métier par une machine à état et que celle-ci a cinq états :
- à faire
- en cours de traitement
- en test
- terminé
- livré
Alors on peut enregistrer cette information d’état en base de données en utilisant une colonne de texte. On enregistrera dans cette colonne de texte l’une des cinq valeurs représentant notre état : toute autre valeur n’a pas de sens par rapport à la modélisation.
Voici un exemple de modèle Django implémentant cette situation :
# 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 demodels.TextChoices
et permet de définir les valeurs de notre énumération - La classe
Process
définit un modèle Django avec un champétat
de typeTextField
.
Comme j’ai spécifié l’argument choices
sur le champ etat
, Django va activer les comportements suivants :
- Mise en place d’un widget spécifique sur les formulaires : au lieu d’avoir un champ de texte libre nous aurons un sélecteur
- Conversion automatique entre la valeur stockée en base de données (
A FAIRE
dans le cas de notre exemple), et la valeur affichée dans notre interface (À faire
) - Validation au niveau du champ pour s’assurer que la valeur saisie fait bien partie des cinq valeurs possibles
Là où le framework trouve ses limites, c’est que la validation du modèle ne s’exécute pas si on n’utilise pas ModelForm
/ModelSerializer
ou que l’on oublie d’appeler Model.full_clean
.
Donc dans les cas suivants, vous n’avez pas de validation :
- Vous utilisez
Model.bulk_create
ouModel.bulk_update
qui n’appellent pasModel.full_clean()
et qui ne font donc pas de validation - Vous utilisez
QuerySet.update
qui n’appelle pasModel.full_clean()
- Vous appelez la méthode
Model.save()
sans avoir appeléModel.full_clean()
- On met à jour la donnée directement en base, sans passer par Django
Donc, si nous n’utilisons pas ModelForm
/ModelSerializer
il y a pas mal de trous dans la raquette en termes de validation !
Pour se protéger contre des valeurs invalides dans la colonne etat
, mettre en place une contrainte SQL est intéressant qu’il faut vérifier côté SGBD (système de gestion de base de données).
Et Django peut nous aider pour ça ! On va pouvoir utiliser le système de migration pour installer/désinstaller la contrainte et nous allons utiliser l’ORM pour exprimer celle-ci.
Installation de la contrainte
La déclaration d’une contrainte SQL se fait via l’attribut 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 instancier deux types d’objets dans constraints
:
- Des instances de
CheckConstraint
- Des instances de
UniqueConstraint
Nous devons définir l’attribut name
de notre contrainte à l’instanciation et celui-ci doit être unique.
À chaque modification de l’attribut constraints
, il est nécessaire de refaire un makemigrations
.
Écriture de la condition de la contrainte
Lorsque l’on instancie une CheckConstraint
, on doit spécifier la condition qu’il faut vérifier côté SGBD. Pour cela, on peut passer deux types d’objets : des objets Q
ou des objets Expression
.
Les objets Q
servent à représenter des conditions SQL. On s’en sert souvent quand on a besoin de réaliser des ou logiques entre des conditions. Ici nous allons écrire la condition qui vérifie 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éfinit ici une contrainte SQL dont l’expression 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 à ajouter un message d’erreur utile pour l’utilisateur. L’implémentation 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’attribut violation_error_message
définit le message d’erreur qui est remonté dans les interfaces (admin, form, etc.) utilisateurs. Je vous recommande de l’utiliser de manière systématique 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’application Django et du nom de la classe. Le message d’erreur 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 migrations : makemigrations
puis migrate
.
Chaque modification de la classe EtatProcess
(ajout, suppression ou modification de valeurs) entraînera la modification de la contrainte SQL de manière automatique.
Comme la contrainte est installée en base de données, nous sommes donc assurés que la colonne etat
ne contiendra que des valeurs issues de l’enum EtatProcess
!
Pour finir, quelques limites de ce système de vérification :
- On ne peut pas exprimer de contraintes multi-tables avec les
CHECK CONSTRAINT
. - La contrainte doit être immuable (immutable dans la terminologie PostgreSQL). Si vous faites appel à une fonction SQL que vous avez écrite dans votre contrainte, et que vous modifiez votre fonction vous cassez votre base. En effet PostgreSQL n’exécute pas la contrainte de vérification sur les lignes existantes dans votre table, uniquement sur les INSERT/UPDATE.
Formations associées
Formations Django
Formation Django initiation
Nantes Du 11 au 13 mars 2025
Voir la Formation Django initiationFormations 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 FrameworkActualités en lien
Makina Corpus est sponsor de la PyConFR 2024
Python
21/10/2024
Le soutien de Makina Corpus à la PyConFR 2024, qui se tient du 31 octobre au 3 novembre 2024 à Strasbourg, reflète ses valeurs de partage et d’innovation, et son engagement envers la communauté dynamique et ouverte de Python.
Revoir les webinaires : découverte de l’outil CANARI-France
Application Web & Mobile
10/04/2024
L’application CANARI-France est destiné aux acteurs agricoles afin de calculer des indicateurs agro-climatiques à partir de projections climatiques. Découvrer en le replay des 4 webinaires organisés par Solagro et l’ADEME.
Utiliser des fonctions PostgreSQL dans des contraintes Django
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.