Retour au cours

data / sqlalchemy

Types de colonnes personnalisés avec TypeDecorator

Leçon 171 exercice

Explication

Ce que vous allez apprendre

  • Comprendre les limites des types de colonnes standards face à des besoins non couverts
  • Créer un type sur mesure avec TypeDecorator en s'appuyant sur un type SQL existant
  • Transformer une valeur avant l'écriture avec process_bind_param
  • Reconstruire la valeur Python d'origine à la lecture avec process_result_value
  • Reconnaître un usage adapté au chiffrement transparent d'une donnée sensible

Dans quel contexte ?

Une application doit stocker une liste de tags Python (["orm", "python", "backend"]) associée à chaque article, alors qu'aucune colonne SQL native ne représente directement une liste de chaînes dans certaines bases. Plutôt que de sérialiser/désérialiser manuellement du JSON à chaque lecture et écriture partout dans le code, un TypeDecorator encapsule cette transformation une seule fois, dans la définition du type lui-même.

D'abord, les limites des types standards

La leçon 2 a présenté les types de colonnes standards (String, Numeric, JSON, Enum...). Mais certains besoins n'ont pas d'équivalent SQL direct : stocker une liste Python dans une colonne texte, ou chiffrer une donnée sensible automatiquement.

La solution : un type sur mesure

TypeDecorator permet de créer son propre type qui s'appuie sur un type SQL existant tout en ajoutant une transformation personnalisée, invisible pour le reste du code.

MéthodeMomentRôle
process_bind_paramjuste avant l'écriturePython → représentation stockée
process_result_valuejuste après la lecturereprésentation stockée → Python

Étape 1 : transformer avant l'écriture

process_bind_param s'exécute juste avant l'écriture en base : elle transforme la valeur Python en ce qui doit réellement être stocké, par exemple convertir une liste en texte JSON.

Étape 2 : transformer à la lecture, en miroir

process_result_value fait l'inverse à la lecture : elle reconstruit la valeur Python d'origine à partir de ce qui est stocké. Le code applicatif ne voit jamais la représentation stockée, seulement l'objet Python riche.

Le vrai bénéfice : une transparence totale

On assigne article.tags = ["orm", "python"] comme une liste normale, on relit article.tags comme une liste normale. Le fait que ce soit stocké sous forme de JSON est un détail encapsulé dans le type, pas une préoccupation qui doit se répéter partout où la colonne est utilisée.

Un usage sensible à connaître

Chiffrer une donnée via un TypeDecorator garantit qu'elle n'est jamais manipulée en clair par erreur dans le code métier, tout en restant lisible normalement côté Python — un bon compromis à réserver aux données réellement sensibles.

Le saviez-vous ?

Un TypeDecorator de chiffrement transparent (numéro de carte, numéro de sécurité sociale) garantit qu'aucun développeur ne peut accidentellement écrire une valeur en clair : la transformation se fait automatiquement à chaque écriture, sans dépendre de la discipline de chacun.

Vers la suite

Après avoir personnalisé le stockage d'une colonne, la prochaine leçon change d'échelle : que faire quand une seule base de données ne suffit plus du tout, et qu'il faut répartir les données sur plusieurs serveurs.

Commandes & code

Types de colonnes personnalisés (TypeDecorator)

python
import json
from sqlalchemy import String, Numeric, TypeDecorator
from sqlalchemy.orm import Mapped, mapped_column

# TypeDecorator : transforme une valeur Python <-> sa représentation stockée en base
class ListeDeChaines(TypeDecorator):
    # Stocke une list[str] Python sous forme de JSON dans une colonne TEXT
    impl = String
    cache_ok = True   # indique à SQLAlchemy que ce type est sûr à mettre en cache (perf des requêtes)

    def process_bind_param(self, value, dialect):
        # Appelé juste avant l'écriture en base : Python -> SQL
        if value is None:
            return None
        return json.dumps(value)

    def process_result_value(self, value, dialect):
        # Appelé juste après la lecture depuis la base : SQL -> Python
        if value is None:
            return None
        return json.loads(value)

class Article(Base):
    __tablename__ = "articles"
    id: Mapped[int] = mapped_column(primary_key=True)
    titre: Mapped[str]
    tags: Mapped[list[str]] = mapped_column(ListeDeChaines(500), default=list)

# Utilisation transparente : on manipule une vraie liste Python, jamais le JSON brut
article = Article(titre="Guide SQLAlchemy", tags=["orm", "python", "sql"])
session.add(article)
session.commit()
session.refresh(article)
assert article.tags == ["orm", "python", "sql"]   # round-trip transparent

# TypeDecorator avec chiffrement transparent (colonne sensible, ex: numéro de pièce d'identité)
class ValeurChiffree(TypeDecorator):
    impl = String
    cache_ok = True

    def __init__(self, cle: bytes, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._fernet_cle = cle   # instancier Fernet(cle) réellement dans un projet, simplifié ici

    def process_bind_param(self, value, dialect):
        if value is None:
            return None
        return chiffrer(value, self._fernet_cle)      # ex: Fernet(self._fernet_cle).encrypt(...)

    def process_result_value(self, value, dialect):
        if value is None:
            return None
        return dechiffrer(value, self._fernet_cle)

class Utilisateur(Base):
    __tablename__ = "utilisateurs"
    id: Mapped[int] = mapped_column(primary_key=True)
    numero_sensible: Mapped[str] = mapped_column(ValeurChiffree(CLE_CHIFFREMENT, 255))
# En base : une chaîne chiffrée illisible ; en Python : la valeur en clair, automatiquement

# TypeDecorator qui n'intervient que sur la LECTURE (coercion pour l'API, ex: arrondi monétaire)
class DecimalArrondi(TypeDecorator):
    impl = Numeric(10, 2)
    cache_ok = True

    def process_result_value(self, value, dialect):
        return round(float(value), 2) if value is not None else None

# TypeDecorator qui contrôle le rendu quand la valeur apparaît en dur dans le SQL généré (rare, debug)
class StatutMajuscule(TypeDecorator):
    impl = String
    cache_ok = True

    def process_bind_param(self, value, dialect):
        return value.upper() if value is not None else None

    def process_result_value(self, value, dialect):
        return value.lower() if value is not None else None

Résumé

  • TypeDecorator encapsule la conversion Python <-> SQL dans process_bind_param (écriture) et process_result_value (lecture).
  • impl déclare le type SQL réellement stocké ; cache_ok = True autorise SQLAlchemy à mettre en cache les requêtes générées avec ce type.
  • Idéal pour : sérialisation JSON dans une colonne texte, chiffrement transparent, normalisation systématique (arrondi, casse).
  • Le code applicatif manipule toujours le type Python riche (liste, valeur déchiffrée) sans jamais voir la représentation stockée.

Exercices pratiques

1 disponible
1

Mission : un correctif support qui contourne le chiffrement transparent

Objectif : Diagnostiquer pourquoi une correction SQL brute a stocké une donnée sensible en clair malgré un TypeDecorator de chiffrement, puis rétablir un correctif qui passe par l'ORM.

Contexte

Le modèle Utilisateur stocke numero_sensible via le type ValeurChiffree(TypeDecorator), qui chiffre automatiquement la valeur avant écriture et la déchiffre à la lecture. Pour corriger rapidement une faute de frappe signalée par le support, un développeur pressé exécute session.execute(text("UPDATE utilisateurs SET numero_sensible = :val WHERE id = :id"), {"val": "1234567890", "id": 42}) directement. En relisant la ligne en base, l'équipe sécurité découvre que la valeur est stockée en clair, alors que toutes les autres lignes sont chiffrées.

Résoudre l’exercice →