Makina Blog

Le blog Makina-corpus

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.

  1. 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).

  2. 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.

  3. 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 formation

Actualités en lien

Image
Encart Django
06/11/2023

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.

Voir l'article
Image
Agrégateur Geotrek
08/06/2023

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.

Voir l'article
Image
Générer les urls Django à partir de la structure des dossiers
23/05/2023

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.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus