L'adoption naissante du Cloud et surtout le buzz qui l'entoure apportent aux développeurs et architectes de nouvelles idées qui secouent certaines habitudes qu'on croyait aller de soi.
Le composant qui souffre le plus est un de ceux qui semble pourtant le mieux installé dans la durée : la base de données relationnelle.
A peu près tous les fournisseurs de cloud proposent en effet un service de stockage dans lequel le côté "relationnel" a disparu en même temps que le langage de requête SQL. C'est le cas entre autres de SimpleDB d'Amazon, BigTable de Google et Azure Storage de Microsoft (et non le très relationnel MS SQL Data Service), qui ont en commun de booster la disponibilité et la partitionabilité au dépend de la consistance des données, conformément au théorème de CAP (cf. USI-2009/Le Touilleur Express).
Evidement, constater que tous ces grands du Web convergent vers le même type de solution a de quoi faire réfléchir, surtout quand on sait que ce type de stockage, simple d'emploi et capable de monter en puissance sans explosion des coûts ("scalable", à proposer d'urgence à l'Académie Française), est déjà utilisé avec succès en interne par Amazon et Google.
"Faire réfléchir" c'est peu dire. Avec la création récente de NoSQL, le Cloud semble agir comme un catalyseur suceptible de déclencher un tsunami anti-base de données relationnelle. NoSQL rassemble des acteurs désireux de faire connaitre leurs systèmes de stockage open-source non relationnels, parfois très proche d'un simple dictionnaire constitué de paires clé-valeur. On y trouve CouchDB, Voldemort (j'aime bien le nom), Cassandra, Dynomite, HBase, HyperTable, VPork (joli aussi) et MongoDB.
Si ces systèmes de gestion des données gagnent du terrain, il faudra sans doute oublier ou en tout cas repenser l'approche centrée sur le modèle relationnel qui caractérise la grande majorité des architectures des applications de gestion de données. Certains mots-clés comme "SGBDR", voire "ORM" et "Hibernate" pourraient devenir aussi datés que le WAP (oouuouuh, tremblez).
Tous les liens vers les solutions de persistance NoSQL sont sur le compte-rendu de la première rencontre NoSQL qui a eu lieu le 12 juin 2009 et a rassemblé 150 personnes à San Francisco :
Ayende Rahien a annoncé samedi sur son blog la sortie de la version 2.0 de NHibernate. Cette version propose les mêmes fonctionnalités que son pendant Java (Hibernate 3.2) , plus quelques particularités uniques à .Net.
Pendant ce temps, Microsoft livre la première version release de son outil ORM Entity Framework en le joignant au SP1 de .Net 3.5.
C'est donc le moment de savourer la liberté de choisir son ORM. Et de prendre le temps de comparer.
Contrairement à Entity Framework (en tout cas, contrairement à ce que Diego Vega annonce pour Entity Framework), LINQ to SQL possède le même comportement que NHibernate vis-à-vis du lazy loading : les requêtes SQL peuvent être déclenchées à l'insu du développpeur dès qu'une collection initialisée paresseusement est énumérée. Ce comportement peut ruiner les performances d'un système. En tout cas il fait sauter au plafond les DBA qui voient passer les requêtes SQL.
La solution apportée par NHibernate consiste pour le développeur à insérer le mot-clé "fetch" dans les requêtes HQL pour forcer le chargement en une seule requête (eager loading) d'une collection d'objets associés.
La solution apportée par LINQ to SQL consiste à préciser au contexte de données les associations à fetcher via la méthode LoadWith de DataLoadOptions.
Par exemple le code suivant, adaptation d'un exemple qui accompagne le livre LINQ In Action, va exécuter autant de requête SQL que de sujets présents dans la table Subjects. Pas bien, pas beau :
public void LazyLoadingChildren_6_25() { DataContext dataContext = new DataContext(Properties.Settings.Default.liaConnectionString); dataContext.Log = Console.Out;
Table subjects = dataContext.GetTable(); foreach (Subject l_subject in subjects) { ObjectDumper.Write("Subject : " + l_subject.Name); foreach (Book l_book in l_subject.Books) { ObjectDumper.Write(" Book title : " + l_book.Title); } } }
Précisons maintenant au contexte que toute association one-to-many (ou 1:n) des Subjects vers les Books doit être chargée en une fois :
public void LazyLoadingChildren_6_25_fetch books() { DataContext dataContext = new DataContext(Properties.Settings.Default.liaConnectionString); dataContext.Log = Console.Out;
DataLoadOptions options = new DataLoadOptions(); options.LoadWith(Subject => Subject.Books); dataContext.LoadOptions = options;
Table subjects = dataContext.GetTable(); foreach (Subject l_subject in subjects) { ObjectDumper.Write("Subject : " + l_subject.Name); foreach (Book l_book in l_subject.Books) { ObjectDumper.Write(" Book title : " + l_book.Title); } } }
La database ne reçoit maintenant qu'une seule requête SQL. Ouf. Mais...
Fetch multiple --> bug ?
Si maintenant j'ai deux boucles imbriquées, la doc de MSDN laisse penser que le même mécanisme doit s'appliquer. Le code suivant devrait n'exécuter qu'une seule requête :
public void LazyLoadingChildren_6_25() { DataContext dataContext = new DataContext(Properties.Settings.Default.liaConnectionString); dataContext.Log = Console.Out;
DataLoadOptions options = new DataLoadOptions(); options.LoadWith(subject => subject.Books); options.LoadWith(book => book.Reviews);
dataContext.LoadOptions = options;
Table subjects = dataContext.GetTable(); foreach (Subject l_subject in subjects) { ObjectDumper.Write("Subject : " + l_subject.Name); foreach (Book l_book in l_subject.Books) { ObjectDumper.Write(" Book title : " + l_book.Title); foreach (Review l_review in l_book.Reviews) { ObjectDumper.Write(" Review " + l_review.Comments); }
}
}
}
Malheureusement ce n'est pas le cas, je vois passer plusieurs requêtes (toujours grâce au projet VS 2008 du livre LINQ in Action), tout se passe comme si LINQ to SQL ne savait pas traiter en eager loading de multiples associations emboitées 1:n.
[Mise à jour] Ce problème a été reporté à l'équipe Linq et a été clos car jugé "by design" en septembre 2007. A garder en tête pour éviter de grever les performances.
Hibernate et son cousin .Net NHibernate souffrent d’un handicap qui nuit à leur réputation. Dès 2006 Sami Jaber constatait que les premiers retours terrain étaient plutôt désastreux : "[...] des requêtes SQL générées de 70 pages imprimées (véridique), parfois 300 ou 400 requêtes par formulaires et des DBA qui crient au scandale".
Implicit Lazy loading
Le lazy loading implicite (littéralement "chargement paresseux") mis en œuvre par (N)Hibernate est en grande partie responsable de cette notoriété reprochable. Typiquement en lazy loading, un objet (disons client) dont les propriétés ont été renseignées par NHibernate verra ses objets associés (disons client.Commandes) initialisés paresseusement par une requête SQL, c'est-à-dire seulement quand nécessaire et si nécessaire.
Lazy road crossing. Rien à voir. Désolé.
Jusque là tout va bien, le lazy loading c'est plutôt bien pour éviter que toute la base de données ne se retrouve en mémoire par le biais de plusieurs associations.
Le hic, c'est que toute cette mécanique de requêtage automatique de la base de données se produit implicitement, presque à l'insu du développeur. Par exemple si le développeur doit écrire une itération sur tous les client.Commandes pour calculer le total des commandes, il écrira une boucle foreach (Commande commande in client.Commandes), et NHibernate va silencieusement générer et exécuter autant de requêtes SQL que de commandes associées à cet objet client. 150 commandes ? 150 requêtes envoyées une-à-une à la base de données...
D'un côté c'est génial parce que le développeur peut manipuler des objets C# purs et durs en ignorant qu'une base de données est à l'œuvre derrière le décor, orchestrée par NHibernate. De l'autre, et pour les même raisons, c'est une catastrophe. En ignorant la réalité du SGDB, les performances chutent et les DBA crient au scandale, parce que la façon optimale de charger des données en mémoire dépend, au cas par cas, de l'usage que l'on s'apprête à en faire. NHibernate n'ayant aucune vision de cet usage, l'optimisation des requêtes ne peut être menée que par le développeur grâce aux outils (join fetch par exemple) de NHibernate.
Malheureusement, quelles que soient les possibilités qu'offre NHibernate pour forcer le chargement d'une collection d'objets en un seul aller-retour SQL, le seul fait que le développeur ait la possibilité d'ignorer le moyen par lequel les objets persistent (conformément au principe de Persistence Ignorance –PI– de l'approche Domain Driven Design –DDD) promet quelques utilisateurs énervés par la lenteur de certains traitements, des DBA scandalisés et finalement des clients mécontents. Bref une sale réputation.
Explicit loading
Et c'est donc une décision marketing qui a conduit l'équipe Microsoft responsable d'Entity Framework à choisir le chargement explicite (explicit loading) plutôt qu'implicite. Dans le dernier numéro de MSDN Magazine, l'encart intitulé "Insights: Entity Framework Data Loading" et signé DiegoVega est clair à ce propos : " Following the "no hidden network roundtrips" principle, Entity Framework avoids automatic lazy loading".
Aucun aller-retour avec le SGBD n'aura lieu sans que le développeur ne le sache.
Le principe de l'explicit loading est simple : tant que le développeur n'a pas écrit le nécessaire pour charger une Commande de notre association client.Commandes, l'accès à une de ces commandes déclenche une exception. En explicit loading, les développeurs se soucient du SGBD, les DBA sont heureux, et l'image de Microsoft est sauve.
Entity Framework, la couche de mapping objets/base de données relationelle (ORM) de .Net actuellement en beta 3, sera multi-database comme l'est Nhibernate. On pourra basculer de SQL Server vers MySql aussi facilement qu'on change de T-shirt (oui, vous faites bien de vous méfier de ce genre de promesses...).
Pour preuve, ces éditeurs cités par InfoQ prévoient de livrer des providers ADO.Net compatibles Entity Framework pour Oracle, MySQL, PostgreSQL, SQLite et DB2 : http://www.infoq.com/news/2008/05/ADO.NETProvider
Les commentaires récents