Dot.Blog

Consulting DotNet C#, XAML, WinUI, WPF, MAUI, IA

Pourquoi avons-nous inventé les ORM ?

La persistance objet avant Entity Framework, chez Microsoft et ailleurs.

Série — Entity Framework : remettre les compteurs à zéro
Partie 1 sur 4. Cet article revient sur le problème que les ORM tentent de résoudre. Il prépare l'histoire d'Entity Framework sans en faire encore le sujet principal.

Un objet n'est pas une ligne

Une classe C# et une table SQL peuvent sembler proches : des propriétés d'un côté, des colonnes de l'autre. Cette ressemblance est trompeuse.

Considérons un modèle métier élémentaire :

public sealed class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public List<Order> Orders { get; set; } = [];
}

public sealed class Order
{
    public int Id { get; set; }
    public DateTime CreatedAtUtc { get; set; }
    public OrderStatus Status { get; set; }
    public List<OrderLine> Lines { get; set; } = [];
}

Dans le programme, un client référence une collection de commandes. Une commande contient des lignes. Chaque instance possède une identité et un cycle de vie. Le développeur navigue dans un graphe.

Dans la base relationnelle, les mêmes données sont distribuées entre des tables. Les relations reposent sur des clés étrangères. Retrouver le graphe nécessite des jointures ; enregistrer ses modifications nécessite de respecter un ordre, des contraintes et une transaction.

Figure 3 — L'ORM ne fait pas seulement correspondre des propriétés à des colonnes : il reconstruit et synchronise un graphe à partir d'ensembles de lignes.

Le terme classique est object-relational impedance mismatch, le décalage objet-relationnel. Il regroupe plusieurs difficultés :

  • l'identité d'un objet n'est pas simplement son emplacement en mémoire ;
  • une référence objet devient une clé étrangère ;
  • une collection devient une requête ;
  • l'héritage n'a pas un équivalent relationnel unique ;
  • un type valeur riche peut être réparti entre plusieurs colonnes ;
  • le chargement partiel d'un graphe pose la question de ce qui est présent ou absent ;
  • la modification d'un objet doit devenir un ensemble minimal d'ordres SQL ;
  • deux utilisateurs peuvent modifier la même ligne entre lecture et écriture.

La documentation de Hibernate résume l'objectif historique de l'ORM comme la suppression du code fragile et non typé afin de rendre les grands programmes plus maintenables [1]. Cette ambition est plus précise que « ne plus écrire de SQL ».

Ce que coûte réellement le mapping manuel

Le SQL explicite est robuste et prévisible. Il ne faut pas le caricaturer. Une lecture ADO.NET correctement écrite est parfaitement saine :

await using var command = connection.CreateCommand();
command.CommandText = """
    SELECT Id, Name
    FROM Customers
    WHERE Id = @id;
    """;

var idParameter = command.CreateParameter();
idParameter.ParameterName = "@id";
idParameter.Value = customerId;
command.Parameters.Add(idParameter);

await using var reader = await command.ExecuteReaderAsync();

Customer? customer = null;

if (await reader.ReadAsync())
{
    customer = new Customer
    {
        Id = reader.GetInt32(0),
        Name = reader.GetString(1)
    };
}

Ce code a plusieurs qualités : la requête est visible, les colonnes sont choisies et l'exécution ne comporte pas de mécanisme implicite. Mais le coût ne se limite pas à ces vingt lignes. Il faut encore :

  1. reconstruire les commandes du client ;
  2. éviter de créer plusieurs instances représentant la même ligne ;
  3. détecter les objets nouveaux, modifiés ou supprimés ;
  4. générer les INSERT, UPDATE et DELETE dans le bon ordre ;
  5. convertir les valeurs et traiter les NULL ;
  6. gérer la transaction ;
  7. détecter les conflits de concurrence ;
  8. maintenir le mapping après chaque évolution du schéma.

Le mapping manuel ne devient pas mauvais pour autant. Il devient coûteux lorsque ces opérations se répètent sur des dizaines d'entités et des centaines de requêtes.

Les patterns existaient avant les produits

Les ORM n'ont pas inventé toutes leurs idées. Ils industrialisent des patterns de persistance déjà identifiés.

Active Record

L'objet porte lui-même les opérations de persistance :

public sealed class Customer
{
    public int Id { get; private set; }
    public required string Name { get; set; }

    public Task SaveAsync(CancellationToken cancellationToken = default)
    {
        // La classe connaît directement son stockage.
        throw new NotImplementedException();
    }
}

Cette approche est directe et efficace pour des applications simples. Elle couple cependant le modèle métier à la persistance et devient moins confortable lorsque les transactions impliquent plusieurs types.

Data Mapper

Le mapping est confié à un composant séparé :

public interface ICustomerMapper
{
    Task<Customer?> FindAsync(
        int id,
        CancellationToken cancellationToken = default);

    Task InsertAsync(
        Customer customer,
        CancellationToken cancellationToken = default);
}

Le domaine ignore alors les détails de stockage. Les ORM complets, dont Entity Framework et Hibernate, se rapprochent généralement de cette famille.

Unit of Work

Une unité de travail mémorise les changements effectués pendant une opération métier et les synchronise avec la base au moment approprié. La documentation Microsoft reprend la définition de Martin Fowler : une unité de travail « keeps track of everything you do during a business transaction » [2]. Avec EF Core, DbContext remplit précisément ce rôle.

Identity Map

Si deux requêtes d'une même unité de travail rencontrent la ligne Customers.Id = 42, elles devraient généralement obtenir la même instance suivie. Sans cette garantie, deux représentations contradictoires du même client peuvent coexister en mémoire.

Ces patterns permettent de comprendre les services rendus par un ORM sans les attribuer à une quelconque magie.

De l'ORM complet au micro-ORM

Toutes les bibliothèques de persistance ne choisissent pas le même niveau d'automatisation.

Approche Ce qu'elle automatise Ce qu'elle laisse au développeur
ADO.NET Connexion, commande, lecteur, paramètres SQL, mapping, suivi et unité de travail
Micro-ORM Exécution et mapping des résultats SQL, stratégie de chargement, changements
ORM complet Mapping, requêtes, relations, suivi, migrations Modèle, intentions, contrôle du SQL et optimisation
Active Record Persistance attachée au modèle Organisation métier et requêtes spécifiques

Dapper, apparu plus tard dans l'écosystème .NET, occupe volontairement une position intermédiaire : le développeur écrit le SQL, tandis que la bibliothèque matérialise les résultats. Entity Framework prend en charge un périmètre plus large, au prix d'une abstraction plus importante.

La question pertinente n'est donc pas « ORM ou pas ORM ? », mais « quelle partie de la plomberie voulons-nous automatiser dans ce contexte ? »

Hors de Microsoft : plusieurs réponses au même problème

Les premières solutions objet-relationnelles sont antérieures à .NET. Les bases objet, les outils Smalltalk, TopLink, puis Hibernate dans l'écosystème Java ont exploré différentes manières de préserver une programmation orientée objet au-dessus d'un stockage relationnel.

Hibernate a particulièrement influencé la perception moderne des ORM : classes persistantes ordinaires, sessions, requêtes sur le modèle objet, suivi des changements et mapping configurable. NHibernate a porté cette famille d'idées vers .NET.

Il serait toutefois incorrect de raconter une progression linéaire où chaque nouvel outil remplace le précédent. Les approches ont coexisté parce qu'elles optimisent des besoins différents, alimentant encore plus la confusion dans les souvenirs :

  • application métier orientée domaine ;
  • reporting centré sur le modèle relationnel ;
  • traitement par lots ;
  • lecture à très fort volume ;
  • base héritée dominée par les procédures stockées ;
  • produit multiplateforme devant prendre en charge plusieurs moteurs.

Même la documentation Hibernate actuelle insiste sur un point souvent oublié : son objectif n'est pas de cacher SQL ou le modèle relationnel [3]. Cette observation vaut aussi pour EF Core.

Chez Microsoft : avant Entity Framework

ADO puis ADO.NET

ADO.NET fournit les primitives fondamentales : connexions, commandes, transactions, paramètres, lecteurs et adaptateurs. Son modèle est explicite et proche de la base. Il reste encore aujourd'hui la fondation de nombreux fournisseurs .NET.

await using var transaction = await connection.BeginTransactionAsync();

try
{
    // Commandes explicites utilisant la même connexion et la même transaction.
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

ADO.NET ne promet pas de résoudre le décalage objet-relationnel. Il donne les moyens de communiquer avec le moteur.

DataSet et DataTable

Le DataSet représente des données relationnelles déconnectées en mémoire : tables, colonnes, relations et contraintes. Il s'intégrait remarquablement bien aux designers et au data binding de l'époque.

var dataSet = new DataSet();
var customers = new DataTable("Customers");

customers.Columns.Add("Id", typeof(int));
customers.Columns.Add("Name", typeof(string));

dataSet.Tables.Add(customers);

Cette représentation est centrée sur les données, pas sur un domaine objet riche. Les DataSets typés ont ajouté génération de code et sécurité de typage, tout en conservant cette philosophie tabulaire.

LINQ to SQL

LINQ to SQL a introduit une rupture importante : interroger SQL Server avec des expressions LINQ fortement typées et matérialiser des objets. Son périmètre était plus simple que celui d'Entity Framework et son intégration au langage séduisante.

var activeCustomers =
    from customer in dataContext.Customers
    where customer.IsActive
    orderby customer.Name
    select customer;

LINQ to SQL n'est pas « EF avant EF ». C'est une technologie distincte, largement centrée sur SQL Server et sur un mapping assez direct entre tables et classes.

NHibernate

Avant la maturité d'Entity Framework, NHibernate constituait déjà un ORM complet crédible sur .NET. Il proposait sessions, mappings, lazy loading, requêtes objet et unité de travail. Sa présence montre qu'Entity Framework n'est pas né dans un vide technique ; Microsoft entrait dans un domaine déjà exploré.

Figure 4 — Les outils de persistance forment un continuum. Davantage d'automatisation réduit la plomberie, mais augmente la nécessité de comprendre le comportement de l'abstraction.

L'ambition particulière d'Entity Framework

La première ambition d'Entity Framework dépassait le simple mapping table-classe. Dans une présentation de la Beta 1 d'« Orcas » publiée en 2007, John Papa expliquait que le framework permettait de travailler avec un modèle conceptuel plutôt qu'avec le schéma relationnel logique [4].

Le système distinguait :

  • un modèle conceptuel représentant les entités utilisées par l'application ;
  • un modèle de stockage représentant la base ;
  • une couche de mapping reliant les deux.

Cette ambition explique à la fois la puissance et la lourdeur des premières versions. Entity Framework ne cherchait pas uniquement à économiser du code de mapping. Il cherchait à introduire un modèle de données conceptuel indépendant de l'organisation physique.

La documentation historique définit d'ailleurs l'ADO.NET Entity Framework comme un ensemble de technologies permettant de programmer contre le modèle conceptuel de l'application plutôt que directement contre le schéma relationnel [5].

SQL est resté parce qu'il reste nécessaire

Dire que SQL n'a pas évolué depuis trente ans serait inexact. Les fonctions analytiques, les expressions de table communes, les types JSON, les tables temporelles et de nombreuses capacités propres aux moteurs ont profondément enrichi le langage et ses implémentations, jusqu'aux extensions vectorielles pour le RAG.

Ce qui est resté stable est son socle mental : ensembles, projections, filtres, jointures, agrégations, contraintes et transactions. Cette stabilité rend le SQL explicite et rassurant. Une requête validée et correctement indexée peut vivre longtemps.

Mais écrire tout le code de persistance en SQL ne supprime pas les problèmes de modèle, de transaction ou de concurrence. Cela déplace leur résolution dans le code applicatif et dans la base.

Un ORM est utile lorsqu'il :

  • réduit effectivement la répétition ;
  • conserve un SQL raisonnable ;
  • rend les frontières transactionnelles lisibles ;
  • autorise une échappatoire vers SQL ;
  • reste observable ;
  • ne prétend pas rendre le moteur sans importance.

Il devient nuisible lorsqu'il est utilisé pour éviter d'apprendre la base relationnelle.

À retenir

  • Le problème central n'est pas la syntaxe SQL, mais la synchronisation entre un graphe d'objets et des ensembles relationnels.
  • ADO.NET est une API d'accès explicite, pas un ORM incomplet.
  • Active Record, Data Mapper, Unit of Work et Identity Map sont des patterns, pas des fonctions propres à EF.
  • Les micro-ORM et les ORM complets occupent des positions différentes sur un continuum d'automatisation.
  • LINQ to SQL et Entity Framework sont des produits distincts.
  • La première ambition d'Entity Framework était un modèle conceptuel séparé du modèle de stockage.
  • Un ORM correctement utilisé ne dispense jamais de comprendre SQL.

Conclusion partielle

La prochaine partie examinera comment cette ambition initiale a produit l'EDMX, pourquoi Code First a changé la source de vérité, et pourquoi EF Core a finalement nécessité une réécriture.
J'espère sincèrement que cette remise à plat à la fois d'une histoire riche en rebondissements et en enseignemens techniques vous donnera, au fil des articles, une vision neuve et complète des ORM dans le monde .NET et ce quest aujoud'hui EF Core, ce qui'il offre vraiment, et ce qu'il n'est pas ou plus.


Stay Tuned!

 

Références

  1. Hibernate ORM, « A Short Guide to Hibernate 7 », introduction à l'ORM.
  2. Microsoft Learn, « DbContext Lifetime, Configuration, and Initialization », citation et application du pattern Unit of Work.
  3. Hibernate ORM, « Hibernate ORM User Guide », préface et objectifs de conception.
  4. John Papa, « ADO.NET Entity Framework Overview », MSDN Magazine, juillet 2007.
  5. Microsoft Open Specifications, « ADO.NET Entity Framework — Glossary ».
  6. Martin Fowler, « Data Mapper », Patterns of Enterprise Application Architecture.
  7. Martin Fowler, « Active Record », Patterns of Enterprise Application Architecture.
  8. Martin Fowler, « Unit of Work », Patterns of Enterprise Application Architecture.
Faites des heureux, PARTAGEZ l'article !