Entity Framework: promesses et faiblesses

Disponible depuis la version 3.5sp1 du framework .net, EntityFramework est un framework open source proposant de gérer les interactions avec n’importe quelle base de données au travers d’un modèle objet.

Promesses

La promesse d’EntityFramework est la suivante: « Quelle que soit la base de données sous-jacente configurée pour stocker et restituer les données, le développeur utilisant EntityFramework ne manipule que des objets ». Pour un usurpateur comme moi, qui a réussi à faire illusion durant plusieurs années dans le monde du développement applicatif sans jamais écrire la moindre requête SQL, cette promesse est une aubaine puisqu’il semble désormais possible d’adresser n’importe quelle base sans en connaître le langage, en ne manipulant que des objets et des méthodes qu’EntityFramework traduira au moment opportun en la requête idoine.

Créer et manipuler son modèle

Concrètement, comment se passe la création du modèle de données avec EntityFramework?

Dès le premier contact, dès cette première étape, on constate que les développeurs d’EF n’ont pas épargné leurs efforts car il n’existe pas moins de 4 approches possibles. Selon que vous soyez plus à l’aise avec du code ou avec un diagramme de classe, EntityFramework vous propose respectivement « CodeFirst » et « ModelFirst » pour générer un modèle de base de données, une instance de cette base et les classes permettant de la manipuler; en partant de votre code (spécifiquement instrumenté pour l’occasion, à l’aide d’attributs adéquats) ou depuis votre modèle objet (décrit dans un diagramme à l’extension *.edmx). A l’inverse, si la base de données existe déjà et que vous ne souhaitez qu’y accéder (ou si vous préférez rester maître de la structure de votre base en la décrivant vous-même) « DatabaseFirst » vous offre de générer à partir d’une base définie, le modèle objet correspondant (sous forme de code ou de diagramme).
Chacun y trouvera donc son bonheur, de l’approche la plus simple à la plus maîtrisée.

Une fois le modèle créé il ne vous reste plus qu’à instancier ce qu’EF appelle un « contexte », c’est à dire un point d’entrée vers votre base, et à utiliser ce contexte pour y pousser des instances d’objets de votre modèle (ajouter des éléments en base) ou y récupérer des collections d’instances d’objets de votre modèle (lire des éléments en base).
Un petit « context.SaveChanges() » plus tard, et toutes les modifications effectuées sur le modèle objet sont répercutées dans la base de données sous-jacente, le tout sans la moindre ligne de « langage BdD »… …ou presque.

int clientId = 42; // l'identifiant du client à récupérer en base
MyContext context = new MyContext();
Client client = context.Clients.Where(client => client.Id == clientId).SingleOrDefault();
client.AccountLocked = false;
context.SaveChanges();

LINQ et Queries

En effet, si EF s’appuie largement sur les méthodes d’extension LINQ, c’est parce qu’il ne peut pas renier sont fort attachement aux concepts propres aux bases de données relationnelles. Ainsi, récupérer une collection d’objets depuis le contexte ne se fera pas sans une méthode telle que « Where() », « FirstOrDefault() » ou encore « Single() ». Si EF unifie bel et bien toutes les bases de données derrière la syntaxe LINQ, on ne pourra pas s’épargner de maîtriser cette dernière. Et pour rendre l’écriture de requêtes (notez le terme emprunté au vocabulaire BdD) encore plus rapide, EF vous propose une syntaxe allégée à base de « from … where … select … » lesquelles seront traduites à l’exécution en requêtes natives du type de la base de données sous-jacente. Et c’est là que les choses se corsent, que vous soyez expert SQL ou non.

var query = from client in context.Clients
where client.Id == clientId
select client;
Client myClient = query.SingleOrDefault();

Pour un néophyte en effet, l’introduction de notions telles que « join » dans une requête qui se veut manipuler une collection d’objets a de quoi déconcerter. Non seulement ce mot clef est inconnu de quiconque n’a jamais fait de BdD, mais son application dans le contexte d’un modèle exclusivement objet nécessite un véritable effort intellectuel pour être correctement appréhendé.

Pour un expert BdD c’est peut être pire, car là où l’expert saura tuner ses requêtes natives à la base pour garantir un temps de réponse optimal et maîtriser au mieux la quantité d’informations remontées, l’utilisation de la « syntaxe EF » et le passage par l’étape de « traduction » du framework ne produit pas toujours la requête native attendue; et le travail d’optimisation peut rapidement prendre la tournure d’une partie de fléchettes à l’aveugle. Oubliez les « LEFT JOIN », « INNER JOIN » et autres « FULL JOIN », vous devrez composer avec les mots clefs proposés par EF et prier pour que la requête native une fois traduite soit celle que vous souhaitiez produire.

A ce titre on peut dire que, dès lors que les usages se complexifient (c’est à dire dès qu’on sort du cadre de l’excellent tutorial proposé sur le site officiel d’EF) la promesse d’Entity Framework n’est pas complètement tenue.

Performance

Le deuxième point sensible à prendre en compte avant de se lancer à corps perdu avec EF, est la performance.

Comme tout framework, celui-ci impose son inévitable surpoids que seule une connaissance des diverses options avancée (précompilation des requêtes par exemple) permet d’atténuer. La récupération d’éléments en base donnant invariablement lieu à la création d’une collection d’objets, on imagine aisément l’impact non négligeable en termes de performances et d’occupation mémoire. Mais là où EF dépasse les craintes, c’est lorsqu’on ne comprend pas parfaitement son fonctionnement et la manière dont les interactions sont réalisées avec la base (c’est à dire, lorsqu’on est le public principalement visé par EF).

Si l’instanciation d’un contexte et la création d’une requête sur ce contexte sont des opérations peu coûteuses, l’exécution de la requête elle-même (qui intervient seulement lorsqu’on tente d’accéder au résultat de la requête, par exemple en itérant sur sa liste d’éléments) représente le véritable coût de l’opération: la requête est traduite, transmise à la base, cette dernière produit des résultats qui sont ensuite convertis en instances de classes du modèle objet.
C’est cette collection d’instances qui va représenter, pour le développeur, les données à lire et éventuellement à mettre à jour.

Persistance du modèle objet

La première chose à comprendre, c’est que ces objets vont vivre indépendamment de la base de données. Une fois créés depuis la base, les objets ne sont pas « rafraichis » à chaque accès (dieu merci!) et les modifications apportées à ces objets ne sont pas non plus impactés en base tant qu’un point de synchronisation n’a pas été explicitement demandé par le développeur. C’est le rôle par exemple de la méthode « DBContext.SaveChanges() » qui va mettre à jour en base -par une unique requête automatiquement générée- l’ensemble des modifications apportées aux entités du modèle objet depuis son chargement (ou sa dernière mise à jour). De plus, afin d’éviter des interactions inutiles avec la base, les résultats d’une requête sont mises en cache et toute exécution ultérieure de la même requête aura pour effet de retourner la collection d’objets existante au lieu de soumettre une nouvelle requête à la BdD. Ces optimisations, contraignantes mais nécessaires, laissent dès lors entrevoir deux dérives possibles:

– Travailler sur des objets obsolètes. En omettant ces points de synchronisation et en travaillant sur des objets en cache, le modèle objet peut avec le temps diverger du contenu réel de la base, et l’on travaille alors avec des données obsolètes et potentiellement erronées. EF recommande d’ailleurs de créer une nouvelle instance de contexte pour chaque opération unitaire (interaction utilisateur, génération d’une page ASP.net, appel d’un WebService…). Afin de forcer le rechargement de données à jour et d’éviter la persistance d’un cache indésirable.

– Multiplier les requêtes. A l’inverse, excès de « prudence » dans la gestion du contexte et de son cache peuvent aboutir à une prolifération d’accès à la BdD sous-jacente, au détriment des performances de l’application. Rien de plus catastrophique par exemple que de placer votre « SaveChanges() » à l’intérieur d’une boucle modifiant les éléments de votre modèle objet. Chaque transition du modèle depuis/vers la base est coûteuse, et EF n’est déjà, à la base, pas avare de ces interactions…

Chargement des données

…En effet, si les notions de « propriétés de navigation » et de « lazy loading » sont familières aux vieux briscards d’EF, ces mots évoquent tout au plus pour le novice l’éventualité d’optimiser facilement le comportement d’EntityFramework. Or, s’il est bien une chose à savoir, c’est qu’un contexte en mode « LazyLoading=true » (le mode par défaut) est tout sauf paresseux dès lors qu’il s’agit de créer des connexions avec la base de données et de multiplier les requêtes.

Que votre modèle objet ait été généré depuis une base de données ou défini par votre code/diagramme, les propriétés qui relient entre eux les objets de votre modèle sont appelées « propriétés de navigation ». Du point de vue objet, ces propriétés ne sont rien d’autres que des associations/compositions au sens UML, mais une fois transposées dans une modélisation de type BdD, ces propriétés représentent bel et bien la possibilité de naviguer d’une table à une autre via une association parent-enfant ou encore many-to-many.

Or, que se passe-t-il lorsque vous récupérez une collection d’objets dans une « table » de votre contexte? EntityFramework crée les objets associés à la table et parcourt récursivement toutes les relations de cette table pour instancier tous les objets définis par ces relations? Bien sûr que non!
Un tel fonctionnement aurait pour conséquence d’instancier un volume de données sensiblement équivalent à la taille totale de votre base (pour peu que celle-ci soit un minimum relationnelle) à chaque requête, ce qui est évidemment exclu. Au lieu de cela, EF propose différentes façons de charger les éléments et leurs dépendances:

– Le chargement avide (EagerLoading): A l’aide de l’instruction « include », vous pouvez spécifier -dans la requête de votre contexte- les relations à parcourir et dont il faut instancier les dépendances. L’exécution de la requête aura alors pour effet de créer tous les objets nécessaires jusqu’au niveau de profondeur spécifié (le tout à l’aide d’un certain nombre de « JOIN » à la complexité variable). Les entités ainsi créées sont donc « complètes » dans le sens où la lecture de leurs propriétés de navigation ne fait que référencer l’instance d’un objet effectivement créé et initialisé. Le chargement est dit « avide » car il va dès le départ chercher toutes les informations qu’il peut (au risque que certaines soient inutiles).

var query = from client in context.Clients
where client.IsActive == true
include client.AccountType
select client;
foreach(Client myClient in query)
{
string type = myClient.AccountType.Type;
}

– Le chargement paresseux (LazyLoading): A l’inverse, sans instruction particulière, votre modèle objet n’aura d’initialisé que les objets « principaux » de la requête (les objets représentant les lignes de la table à laquelle vous accédez). Ce mode est dit « paresseux » car aucun objet inutile n’est ainsi chargé et les informations complémentaires seront récupérées plus tard « au besoin ». En effet, dans ce mode, les propriétés de navigation n’ont pas de valeur initialisée, mais pour autant il est tout à fait possible d’en lire la valeur, et c’est là toute la magie du LazyLoading: A chaque lecture d’une propriété de navigation, une nouvelle requête est soumise à la base pour aller y chercher LA valeur souhaitée. Et c’est bien là le hic: Car une mauvaise utilisation du LazyLoading peut avoir des conséquences catastrophiques pour les performances de votre programme.

var query = from client in context.Clients
where client.IsActive == true
select client;
foreach(Client myClient in query)
{
string type = myClient.AccountType.Type;
}

Imaginez les dégâts de l’extrait de code ci-dessus (duquel on a retiré l’instruction « include », c’est à dire exécuté en mode LazyLoading). D’un simple « foreach », ce code génère autant d’accès à la base de données qu’il y a d’éléments dans votre liste de « Clients » -autrement dit, de lignes dans votre table CLIENT- chaque accès donnant évidemment lieu à la création d’une connexion, la compilation d’une requête, la récupération des résultats, la création du modèle objet associé et la fermeture de la connexion… Je vous laisse imaginer le désastre de performance qu’un code si simple (et suggéré par les tutoriaux EF eux-mêmes) peut engendrer sur des bases de taille « normale », c’est à dire plus que les 100 lignes de votre base de test.

Le lazy loading étant, je vous le rappelle, le mode par défaut d’EntityFramework (et le EagerLoading étant méconnu car étrangement oublié par les supports de formation officiels) bon nombre de débutants auront vite fait de tomber dans le piège. Et si vous pensez que désactiver le chargement paresseux (DBContext.LazyLoading = false;) résout le problème, ne vous faites pas trop d’illusion. Non seulement les propriétés de navigation ne seront pas initialisées par votre requête initiale, mais tenter d’y accéder ne fera que lever une NullReferenceException. Après tout, vous n’avez fait que désactiver la capacité d’EF à aller chercher « Just-In-Time » les informations dont il avait besoin.

La préoccupation de performance liée à l’utilisation d’un framework est donc, dans le cas d’EntityFramework, parfaitement justifiée, et l’utilisation de ce framework à bon escient nécessite un minimum d’expertise dans le domaine. Expertise qu’on ne peut en tout cas pas espérer rencontrer chez un développeur qui se serait contenté des tutoriaux officiels, et n’ayant pas de culture BdD pour appuyer ses réflexions. A ce titre, la promesse n’est, à mon avis, pas tenue.

Bilan

Pour autant, il faut reconnaître les atouts de ce framework qui, une fois maîtrisé, améliore vraiment la productivité à l’aide d’une philosophie de programmation en synergie avec les compétences des développeurs objet. Toute la subtilité d’une bonne utilisation d’EF nécessite dans la présence d’un expert qui saura contenir (grâce à sa connaissance du framework et de la BdD sous-jacente) les éventuelles dégradations de performance, et accompagner les développeurs dans la rédaction de leurs requêtes les plus complexes.

3 thoughts on “Entity Framework: promesses et faiblesses”

  1. Que du bon sens.
    J’ai lu avec intérêt cet article.
    Mon sentiment est que le sql est une des rares techniques qui perdure et que ce n’est pas pour rien.
    A mon avis il y avait d’autres priorités que de réinventer une enième usine à gaz (j’exagère).

  2. Merci vraiment pour ce post qui m’a été très très utile à la prise de décision concernant l’utilisation ou non d’EF.

  3. Super synthèse. Merci. Pour ma part je sors d’une expérience de création d’une solution e-commerce où EF nous a pas mal handicapé d’une manière générale justement parce que son intégration a été décrétée à l’emporte pièce, sans maitrise.
    Lorsqu’il a fallu faire face à la montée en charge, on a pas mal ramé pour maitriser la chose sans DBA dans l’équipe.
    Il s’agissait d’une des toutes premières version d’EF qui faisait par ailleurs porter certaines contraintes sur la modélisation des objets, ça le rendait pas mal impopulaire du côté des développeurs.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

deux − un =