// Bases de données

ORM : mapper sa base de données à des objets

Entre le monde des objets, où l’on manipule des instances de classes, et celui des bases relationnelles, où l’on manipule des lignes et des colonnes, il existe un fossé. L’ORM (Object-Relational Mapping, ou mapping objet-relationnel) est l’outil qui jette un pont au-dessus de ce fossé : il traduit automatiquement vos objets en enregistrements SQL et inversement, pour que vous écriviez du code métier plutôt que des requêtes.

Ce guide explique ce qu’est réellement un ORM, ce qu’il vous fait gagner et ce qu’il vous coûte, puis passe en revue les principaux outils du marché. L’objectif n’est pas de désigner un gagnant, mais de vous donner les critères pour choisir en connaissance de cause.

Le concept en clair

Le problème que résout un ORM

Sans ORM, lire un utilisateur en base ressemble à ça : vous écrivez une requête SQL, vous récupérez un ensemble de lignes, puis vous recopiez manuellement chaque colonne dans un objet de votre langage. Multiplié par toutes les tables d’une application, ce travail de plomberie est répétitif et source d’erreurs.

Un ORM automatise cette correspondance. Vous déclarez une fois que la classe User correspond à la table users, et l’outil se charge du reste : traduire un user.save() en INSERT ou UPDATE, transformer les lignes lues en instances peuplées.

La correspondance de base

L’idée tient en trois équivalences simples, communes à tous les ORM :

  • une classe correspond à une table ;
  • une instance (un objet) correspond à une ligne ;
  • un attribut correspond à une colonne.

Voici, en pseudo-code proche de plusieurs ORM, à quoi ressemble une lecture. Là où vous auriez écrit du SQL, vous appelez une méthode :

-- Ce que vous auriez écrit à la main
SELECT * FROM users WHERE id = 42;
# Ce que vous écrivez avec un ORM (ici SQLAlchemy)
user = session.get(User, 42)
print(user.email)

L’ORM a exécuté un SELECT en coulisses et vous a rendu un objet User prêt à l’emploi.

Avantages et limites face au SQL brut

Ce que l’ORM apporte

Le premier bénéfice est la productivité : les opérations CRUD (créer, lire, mettre à jour, supprimer) tiennent en une ligne, et le code lit comme du métier, pas comme de la plomberie. Le deuxième est la portabilité : le même code fonctionne souvent sur MySQL, PostgreSQL ou SQLite, l’ORM générant le dialecte adapté. Le troisième est la sécurité : les ORM paramètrent les requêtes par défaut, ce qui réduit drastiquement le risque d’injection SQL. S’y ajoutent les migrations versionnées et une meilleure lisibilité pour une équipe.

Ce que l’ORM coûte

Rien n’est gratuit. Un ORM ajoute une couche d’abstraction qui masque le SQL généré : sur une requête complexe, l’outil peut produire une requête sous-optimale sans que vous le voyiez. Il impose une courbe d’apprentissage propre, différente du SQL que vous connaissez déjà. Enfin, il expose à des pièges de performance comme le problème N+1, détaillé plus bas.

La règle réaliste : un ORM excelle sur 90 % des requêtes courantes et vous ralentit sur les 10 % les plus pointues. La plupart des ORM prévoient d’ailleurs une porte de sortie pour écrire du SQL brut quand c’est nécessaire. Pour bien juger le SQL généré, garder de solides bases sur les jointures SQL reste indispensable, ORM ou pas.

Panorama des principaux ORM

Chaque écosystème a ses outils de référence. En voici cinq, présentés de façon neutre : le meilleur est celui qui correspond à votre langage et à vos habitudes.

Prisma (JavaScript / TypeScript)

Prisma est un ORM moderne pour Node.js qui mise sur le typage. Vous décrivez votre schéma dans un fichier dédié, et Prisma génère un client entièrement typé. L’autocomplétion et la vérification à la compilation en font un choix apprécié en TypeScript.

// Récupérer un utilisateur et ses articles avec Prisma
const user = await prisma.user.findUnique({
  where: { id: 42 },
  include: { posts: true },
});

Sequelize (JavaScript / TypeScript)

Sequelize est l’un des ORM les plus anciens de l’écosystème Node. Fondé sur des modèles définis en JavaScript, il couvre MySQL, PostgreSQL, SQLite et d’autres. Il suit le style « active record », où l’objet porte lui-même les méthodes de persistance.

const user = await User.findByPk(42, { include: Post });
await user.update({ email: "[email protected]" });

Doctrine (PHP)

Doctrine est l’ORM de référence dans l’écosystème PHP, notamment avec Symfony. Il applique le patron « data mapper » : les entités sont de simples objets, et un gestionnaire d’entités (EntityManager) orchestre la persistance. Il dispose aussi de son propre langage de requête, le DQL.

$user = $entityManager->find(User::class, 42);
$user->setEmail("[email protected]");
$entityManager->flush();

SQLAlchemy (Python)

SQLAlchemy est l’ORM Python le plus complet. Il se distingue par sa double nature : un cœur bas niveau proche du SQL et une couche ORM par-dessus. Cette flexibilité en fait un outil puissant, au prix d’un apprentissage plus long.

from sqlalchemy import select

stmt = select(User).where(User.age > 18)
majeurs = session.scalars(stmt).all()

Eloquent (PHP / Laravel)

Eloquent est l’ORM intégré au framework Laravel. Adepte du style « active record », il vise avant tout la simplicité d’écriture. Ses relations expressives et sa syntaxe fluide le rendent très rapide à prendre en main.

$user = User::with('posts')->find(42);
$user->email = "[email protected]";
$user->save();

Les migrations

À quoi elles servent

Le schéma d’une base évolue : on ajoute une colonne, on crée une table, on change un type. Les migrations sont des fichiers versionnés qui décrivent ces changements de structure, de façon reproductible et partageable dans le dépôt Git. Chaque membre de l’équipe applique la même suite de migrations et obtient exactement le même schéma.

La plupart des ORM peuvent générer une migration en comparant votre schéma actuel à l’état de la base. Le principe général :

# Exemple conceptuel de migration
def upgrade():
    op.add_column("users", Column("telephone", String(20)))

def downgrade():
    op.drop_column("users", "telephone")

Chaque migration prévoit son inverse (downgrade), ce qui permet de revenir en arrière proprement en cas de problème.

Bonne pratique

Ne modifiez jamais une base de production « à la main » sans passer par une migration. Vous perdriez la trace du changement, et les environnements finiraient par diverger. La migration est la source de vérité de la structure.

Le piège du N+1

En quoi il consiste

Le problème N+1 est le piège de performance le plus classique des ORM. Il survient quand vous chargez une liste d’objets, puis accédez à une relation pour chacun. L’ORM exécute alors une requête pour la liste (le « 1 »), puis une requête par élément pour la relation (les « N »).

# Anti-pattern : 1 requête pour les auteurs + 1 par auteur pour ses livres
auteurs = session.scalars(select(Author)).all()   # 1 requête
for auteur in auteurs:
    print(auteur.livres)   # N requêtes supplémentaires

Avec 100 auteurs, cela fait 101 requêtes là où deux suffiraient. Sur une vraie application, l’effet sur les temps de réponse est brutal.

Comment l’éviter

La solution universelle s’appelle le chargement anticipé (eager loading) : vous demandez à l’ORM de récupérer les relations en une seule fois, via une jointure ou une requête groupée.

from sqlalchemy.orm import selectinload

auteurs = session.scalars(
    select(Author).options(selectinload(Author.livres))
).all()   # 2 requêtes au total, quel que soit le nombre d'auteurs

Chaque ORM a son mot-clé pour cela (include chez Prisma, with chez Eloquent et Doctrine). Prenez le réflexe de vérifier le nombre de requêtes générées : la plupart des outils proposent un mode de journalisation qui les affiche.

Quand utiliser un ORM

Un ORM est le bon choix pour la majorité des applications : celles dont les besoins sont surtout du CRUD, où la vitesse de développement compte, et où l’équipe gagne à lire du code métier plutôt que du SQL. Il brille aussi quand la portabilité entre moteurs a de la valeur.

À l’inverse, restez sur du SQL brut, ou combinez les deux, quand vous écrivez des requêtes analytiques lourdes, quand chaque milliseconde compte sur des rapports complexes, ou quand vous devez exploiter des fonctions spécifiques à un moteur que l’ORM n’expose pas. L’approche mûre n’est pas « ORM contre SQL » mais « ORM pour le courant, SQL brut pour le pointu » — et les deux cohabitent très bien dans un même projet.

En résumé

Un ORM traduit vos objets en lignes de base de données et vous épargne l’essentiel de la plomberie SQL, au prix d’une couche d’abstraction qu’il faut apprendre à maîtriser. Prisma, Sequelize, Doctrine, SQLAlchemy et Eloquent répondent chacun à un écosystème : le choix se fait d’abord par le langage. Quel que soit l’outil, deux réflexes vous éviteront la plupart des ennuis : versionner la structure par des migrations, et traquer le problème N+1 avec du chargement anticipé. Et gardez toujours vos bases de SQL : un ORM les prolonge, il ne les remplace pas.

À lire ensuite