data / sqlalchemy
Types de colonnes personnalisés avec TypeDecorator
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
TypeDecoratoren 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éthode | Moment | Rôle |
|---|---|---|
process_bind_param | juste avant l'écriture | Python → représentation stockée |
process_result_value | juste après la lecture | repré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)
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 NoneRésumé
TypeDecoratorencapsule la conversion Python <-> SQL dansprocess_bind_param(écriture) etprocess_result_value(lecture).impldéclare le type SQL réellement stocké ;cache_ok = Trueautorise 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
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.