Makina Blog
Utiliser des fonctions PostgreSQL dans des contraintes Django
Cet article vous présente comment utiliser les fonctions et les check constraints
PostgreSQL en tant que contrainte sur vos modèles Django.
Lorsque l'on met en œuvre un schéma de données, la mise en place de contraintes SQL correspondantes aux contraintes métier identifiées lors de la modélisation est la clé du succès. En effet, ces contraintes vont assurer la cohérence des données qui sont stockées en base par rapport à la modélisation qui a été prévue.
Lorsque la structure de la base de données est gérée par Django, nous pouvons nous retrouver limiter par l'expressivité de l'ORM ("Mapping objet-relationnel") pour exprimer sa contrainte métier. Une solution est alors de revenir au SQL pour écrire sa contrainte sous la forme d'une fonction renvoyant un booléen. Cependant, il est intéressant d'utiliser l'ORM Django pour installer la contrainte sur sa table. En effet, comme les modèles Django représentent la définition d'une table, il est ergonomique d'y faire figurer la présence de cette contrainte. On pourrait mettre un commentaire/docstring, mais c'est assez inélégant.
Dans cet article, je vous présente une approche permettant d'exposer votre fonction SQL à l'ORM pour ensuite s'en servir dans un CheckConstraint.
Quelques rappels sur les CHECK CONSTRAINT
et PostgreSQL
Avant de mettre en œuvre votre contrainte métier, il est important de s'assurer que la fonction que vous allez écrire vérifie quelques pré-requis.
Vous ne devez surtout pas référencer de données d'une autre table que celle sur laquelle vous mettez en œuvre votre contrainte. En effet, lors d'un
pg_restore
l'ordre de restauration des tables n'est pas défini, et votre contrainte pourrait vous empêcher de restaurer votre base. De même, l'ordre de restauration des lignes dans une table n'étant pas défini, il ne faut pas utiliser le contenu de la table (hormis la ligne étant insérée).Votre fonction doit être absolument
IMMUTABLE
. C'est à dire qu'à partir d'une même ligne votre fonction doit toujours renvoyer le même résultat. La manière la plus courante de violer cette propriété c'est de mettre à jour le corps de la fonction. Il faut alors supprimer et re-installer la contrainte pour que la fonction soit à nouveau exécutée sur chaque ligne de la table.Le fait d'utiliser une fonction pour exprimer une CHECK CONSTRAINT n'est pas obligatoire. Une contrainte PostgreSQL peut s'exprimer directement dans la définition d'une table, sans fonction; mais si votre contrainte est un peu complexe à exprimer, elle sera certainement mieux mise en page (et donc lisible et maintenable) en étant exprimée à travers une fonction. Cependant, le fait de déporter la contrainte dans une fonction ne permet pas de sortir des règles des contraintes, telle que la contrainte numéro 1 ci-dessus.
En résumé, votre fonction SQL doit dépendre uniquement de la ligne qui est insérée/mise à jour en base dans la table. La documentation officielle de PostgreSQL est très éloquente sur le sujet.
⚠️ Si vous avez besoin d'établir des contraintes multi-tables, utilisez les fonctions trigger ⚠️. Sinon, vous pourriez vous retrouver dans cette situation…
Mise en œuvre
Dans un premier temps, nous allons écrire une fonction PostgreSQL et l'installer via le système de migration de Django. Ensuite, nous allons exposer la fonction à l'ORM. Enfin nous utiliserons l'ORM pour ajouter la fonction comme contrainte sur notre table.
Écriture et chargement de la fonction
Commençons par écrire une fonction SQL recevant un identifiant objet et retournant si cet objet vérifie notre contrainte (via un booléen) :
CREATE OR REPLACE FUNCTION ma_contrainte_func (id bigint) RETURNS boolean AS
$FUNC$
SELECT True; -- Nous pourrions utiliser id pour récuperer la ligne correspondant à l'INSERT/UPDATE réalisé afin de réaliser des vérifications plus complexes
$FUNC$
LANGUAGE SQL VOLATILE;
Ensuite, nous pouvons charger cette fonction en base de données via une migration Django :
import pathlib
# Generated by Django 4.2.6 on 2023-10-24 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("mon_application", "0001_une_autre_migration"),
]
operations = [
migrations.RunSQL(
pathlib.Path(
"mon_application/sql/constraints.sql"
).read_text(),
reverse_sql="DROP FUNCTION IF EXISTS ma_contrainte_func(bigint);"
)
]
Notez le paramètre reverse_sql
qui s'occupe de retirer la fonction de la base. Pensez à bien préciser les types des arguments de votre fonction. En effet, ma_contrainte()
et ma_contrainte(bigint)
sont des objets SQL différents.
Branchement de la fonction dans l'ORM
Avant de pouvoir utiliser cette fonction dans nos modèles, il nous faut l'exposer à l'ORM. Pour cela, créez un fichier func.py
dans votre application, et insérez-y le contenu suivant :
from django.db.models import Func, fields
class MaContrainteFunc(Func):
function = "ma_contrainte_func"
arity = 1 # L'arité est le nombre d'argument d'une fonction
output_field = fields.BooleanField()
Vous pouvez maintenant utiliser cette fonction dans votre modèle :
from django.db import models
from django.db.models import CheckConstraint
class MonModel(models.Model):
class Meta:
constraints = [
CheckConstraint(
check=MaContrainteFunc("id"),
name="ma_contrainte",
),
]
Générez une migration avec ./manage.py makemigrations
et lancez la avec ./manage.py migrate
.
Vous pouvez inspecter votre schéma de données, la contrainte devrait être en place sur votre table !
Cette contrainte est une contrainte réalisée par le moteur de base de données lui-même. Cela permet de meilleures performances et un meilleur contrôle des opérations effectuées en dehors de Django sur ces lignes de données (qu'il s'agisse d'un traitement externe ou d'une requête complexe altérant les données de la table).
Note : Django ne génère que des column constraints, là où on peut vouloir mettre en place une table constraints. Sur le moteur de base de données PostgreSQL cela n'est pas un souci car il traite les deux types de contraintes de manière identique.
Formations associées
Formations Django
Formation Django avancé
À distance (FOAD) Du 9 au 13 décembre 2024
Voir la formationActualités en lien
Comment migrer vers une version récente de Django ?
Que ce soit pour avoir les dernières fonctionnalités ou les correctifs de sécurité, rester sur une version récente de Django est important pour la pérennité de son projet.
Le projet Agrégateur : fusionner des bases de données Geotrek
Le partage et la diffusion des données font partie des problématiques historiques au cœur du projet Geotrek.
Générer les urls Django à partir de la structure des dossiers
Dans cet article, nous vous proposons un exemple de génération automatique des urlpatterns
à partir de la structure des fichiers et dossiers contenant les vues.