Aller au contenu
Développement medium

SQLAlchemy, l'ORM Python : modèles, CRUD et relations (2.0)

15 min de lecture

logo SQLAlchemy

SQLAlchemy est l'ORM Python de référence pour dialoguer avec une base de données relationnelle. Un ORM (Object-Relational Mapping) fait correspondre une table à une classe et une ligne à un objet : vous manipulez vos données comme des objets Python au lieu d'écrire du SQL à la main. Ce guide part de zéro et couvre l'installation, la définition de modèles typés en syntaxe SQLAlchemy 2.0, les opérations CRUD et les relations entre tables, sur une base SQLite sans configuration.

Il s'adresse aux développeurs qui découvrent les bases de données en Python et veulent une structure de projet propre, réutilisable ensuite avec FastAPI ou Connexion. Tout le code de cette page a été exécuté avec SQLAlchemy 2.0. La seconde partie enchaîne sur les migrations avec Alembic.

  • Installer SQLAlchemy et le driver adapté à votre base de données.
  • Définir des modèles typés avec la syntaxe moderne Mapped / mapped_column.
  • Structurer un projet SQLAlchemy clair et évolutif.
  • Créer, lire, modifier et supprimer des données (CRUD) avec select().
  • Modéliser des relations entre tables et comprendre la session.
  • Écrire des requêtes sûres, protégées contre les injections SQL.

Un ORM est une couche qui traduit vos objets Python en lignes SQL, et inversement. Sans ORM, vous écrivez des requêtes INSERT, SELECT ou UPDATE sous forme de chaînes de caractères, puis vous convertissez à la main les résultats en objets. C'est répétitif et propice aux erreurs, notamment aux injections SQL quand on concatène des valeurs.

Avec SQLAlchemy, vous décrivez une table par une classe et vous laissez la bibliothèque générer le SQL. Le gain est double : un code plus lisible (on pense en objets métier) et des requêtes paramétrées par défaut, donc plus sûres. Autre avantage décisif : SQLAlchemy s'adapte à la plupart des moteurs, ce qui permet de développer sur SQLite puis de passer en production sur PostgreSQL ou MySQL sans réécrire votre logique.

SQLAlchemy propose deux approches complémentaires. Bien les distinguer évite de se perdre dans la documentation.

CoucheVous manipulezCas d'usage typique
ORMdes objets Python (classes, sessions)applications, CRUD, relations métier
Coredes expressions SQL (Table, select())requêtes très optimisées, ETL, SQL complexe

L'ORM est bâti au-dessus de Core. Pour l'immense majorité des projets, l'ORM suffit et reste plus clair : c'est l'approche que suit ce guide. On descend vers Core uniquement quand on a besoin d'un contrôle précis sur le SQL généré. Les deux couches produisent des requêtes paramétrées, la protection contre les injections est donc acquise dans les deux cas.

L'installation se fait dans un environnement virtuel, pour isoler les dépendances de votre projet. SQLite est intégré à Python : aucun driver supplémentaire n'est nécessaire pour débuter.

Fenêtre de terminal
pip install sqlalchemy

Pour une autre base que SQLite, ajoutez le driver correspondant au moteur. En 2026, le driver PostgreSQL moderne est psycopg (version 3), qui remplace l'ancien psycopg2 :

Fenêtre de terminal
pip install "sqlalchemy" "psycopg[binary]" # PostgreSQL
pip install "sqlalchemy" pymysql # MySQL / MariaDB

Vérifiez que tout est en place. La sortie doit afficher une version commençant par 2. :

import sqlalchemy
print(sqlalchemy.__version__) # 2.0.51

Depuis SQLAlchemy 2.0, un modèle hérite de DeclarativeBase et déclare ses colonnes avec des annotations typées : Mapped[...] donne le type, mapped_column(...) les options. Cette syntaxe remplace l'ancien couple declarative_base() / Column(Integer, ...), encore accepté mais dépassé. Le typage explicite est vérifiable par des outils comme mypy, ce qui attrape des erreurs avant l'exécution.

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(index=True)
age: Mapped[int | None] = mapped_column(default=None)
  • Base : la classe racine dont héritent tous vos modèles.
  • __tablename__ : le nom de la table dans la base.
  • Mapped[int] : une colonne entière non nulle ; Mapped[str | None] la rend nullable.
  • primary_key=True : la clé primaire, auto-incrémentée pour un entier.

Un projet lisible sépare la connexion, les modèles et le point d'entrée. Voici la structure utilisée dans ce guide, adaptée à un petit projet évolutif :

  • Répertoiremy_project/
    • Répertoiredb/
      • init .py
      • database.py
    • Répertoiremodels/
      • init .py
      • user.py
    • main.py

Le fichier db/database.py centralise l'engine (la connexion) et la fabrique de sessions. C'est aussi ici que vit la classe Base, pour éviter les imports circulaires.

db/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
DATABASE_URL = "sqlite:///example.db"
engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
class Base(DeclarativeBase):
pass
  • engine : gère la connexion à la base et le pool de connexions.
  • echo=False : passez à True en développement pour voir le SQL généré. Laissez False en production, car les logs SQL peuvent exposer des données.
  • SessionLocal : la fabrique qui produit les sessions, unités de travail sur la base.
  • expire_on_commit=False : garde les objets utilisables après un commit, pratique pour les afficher juste après.

Le modèle User importe cette Base :

models/user.py
from sqlalchemy.orm import Mapped, mapped_column
from db.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(index=True)
age: Mapped[int | None] = mapped_column(default=None)

Le fichier models/__init__.py réexporte le modèle pour des imports courts :

models/__init__.py
from .user import User
__all__ = ["User"]

Avec les modèles définis, une seule ligne demande à SQLAlchemy de créer les tables manquantes à partir de leurs métadonnées :

main.py
from db.database import Base, engine
from models import User # importe le modèle pour l'enregistrer sur Base
if __name__ == "__main__":
print("Création des tables...")
Base.metadata.create_all(bind=engine)
print("Tables créées.")

À l'exécution, le fichier example.db apparaît dans le dossier courant et contient la table users :

Fenêtre de terminal
python main.py
Création des tables...
Tables créées.

Le CRUD (Create, Read, Update, Delete) est le cœur de toute interaction avec une base. En SQLAlchemy 2.0, tout passe par une session ouverte avec un gestionnaire de contexte (with), qui garantit sa fermeture même en cas d'erreur.

On instancie des objets, on les ajoute à la session, puis on valide avec commit() :

from db.database import SessionLocal
from models import User
def add_users() -> None:
with SessionLocal() as session:
session.add_all([
User(name="Alice", age=25),
User(name="Bob", age=30),
])
session.commit()
print("Utilisateurs ajoutés.")

La syntaxe 2.0 utilise select() exécuté via session.scalars(), qui renvoie directement les objets. Pour une seule ligne par clé primaire, session.get() est le plus direct :

from sqlalchemy import select
from db.database import SessionLocal
from models import User
def get_users() -> None:
with SessionLocal() as session:
for user in session.scalars(select(User).order_by(User.name)):
print(f"ID {user.id} | {user.name} | {user.age} ans")
def get_one(user_id: int) -> None:
with SessionLocal() as session:
user = session.get(User, user_id)
print(user.name if user else "introuvable")

Pour modifier, on récupère l'objet, on change ses attributs, on valide. Pour supprimer, on appelle session.delete() :

def update_user(user_id: int, new_name: str) -> None:
with SessionLocal() as session:
user = session.get(User, user_id)
if user is None:
print(f"Aucun utilisateur {user_id}")
return
user.name = new_name
session.commit()
def delete_user(user_id: int) -> None:
with SessionLocal() as session:
user = session.get(User, user_id)
if user is None:
print(f"Aucun utilisateur {user_id}")
return
session.delete(user)
session.commit()

En enchaînant création, lecture, mise à jour puis suppression, la sortie confirme le cycle complet :

--- après insert ---
ID 1 | Alice | 25 ans
ID 2 | Bob | 30 ans
--- après update de Bob et delete d'Alice ---
ID 2 | Bob Martin | 30 ans

La vraie force d'un ORM apparaît avec les relations. Prenons le cas classique un-à-plusieurs : un utilisateur écrit plusieurs articles. On ajoute une clé étrangère (ForeignKey) côté article et une relationship() de chaque côté pour naviguer entre les objets.

models/user.py
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from db.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(index=True)
age: Mapped[int | None] = mapped_column(default=None)
articles: Mapped[list["Article"]] = relationship(
back_populates="author", cascade="all, delete-orphan"
)
class Article(Base):
__tablename__ = "articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="articles")
  • ForeignKey("users.id") : rattache chaque article à un utilisateur.
  • back_populates : maintient les deux côtés de la relation synchronisés.
  • cascade="all, delete-orphan" : supprimer un utilisateur supprime aussi ses articles, ce qui évite des lignes orphelines.

On crée alors un utilisateur et ses articles en une seule opération, SQLAlchemy s'occupe des clés étrangères :

with SessionLocal() as session:
alice = User(name="Alice", age=25, articles=[Article(title="Premier billet")])
session.add(alice)
session.commit()
print(f"{alice.name} a {len(alice.articles)} article(s)") # Alice a 1 article(s)

La session est une unité de travail : elle regroupe vos opérations et ne les écrit dans la base qu'au commit(). En cas de problème, rollback() annule tout ce qui n'a pas été validé, ce qui protège d'un état incohérent. Le gestionnaire de contexte with SessionLocal() as session: referme automatiquement la session à la fin, y compris si une exception survient.

Côté sécurité, SQLAlchemy vous protège par défaut : les valeurs passées à where() ou aux modèles sont liées (bound parameters), jamais concaténées dans le SQL. Le seul vrai danger vient du SQL brut mal écrit.

from sqlalchemy import text
nom = entree_utilisateur # valeur non fiable
# DANGEREUX : injection SQL possible via f-string
session.execute(text(f"SELECT * FROM users WHERE name = '{nom}'"))
# CORRECT : paramètre lié
session.execute(text("SELECT * FROM users WHERE name = :n"), {"n": nom})

La règle tient en une phrase : jamais de f-string pour insérer une valeur dans une requête, toujours des paramètres liés. Avec l'ORM (select(User).where(User.name == nom)), cette protection est automatique.

  • Un ORM fait correspondre classe et table, objet et ligne : on manipule des objets Python plutôt que du SQL.
  • SQLAlchemy offre deux niveaux : l'ORM (objets, la voie par défaut) et Core (expressions SQL, pour les cas pointus).
  • La syntaxe 2.0 repose sur DeclarativeBase, Mapped et mapped_column : typée et vérifiable par mypy.
  • On lit avec select() + session.scalars() ; session.query() est legacy.
  • Les relations (ForeignKey + relationship) et le cascade évitent le SQL de jointure manuel et les lignes orphelines.
  • La session encadre une transaction : commit() valide, rollback() annule.
  • SQLAlchemy paramètre les requêtes par défaut : ne cassez pas cette protection avec des f-strings dans du SQL brut.
  • create_all ne fait pas évoluer un schéma existant : pour cela, on utilise Alembic.

Votre base vit et votre schéma va évoluer. La suite logique est de versionner ces changements avec Alembic, puis d'exposer vos données via une API.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn