La question "quel ORM devrais-je utiliser" vise vraiment le sommet d'un énorme iceberg en ce qui concerne la stratégie globale d'accès aux données et l'optimisation des performances dans une application à grande échelle.
Conception et maintenance de bases de données
C’est de loin le principal facteur déterminant du débit d’une application ou d’un site Web gérée par les données, et il est souvent totalement ignoré des programmeurs.
Si vous n'utilisez pas les techniques de normalisation appropriées, votre site est condamné. Si vous n'avez pas de clé primaire, presque chaque requête sera lente. Si vous utilisez des anti-modèles connus, tels que l'utilisation de tables pour les paires clé-valeur (valeur d'entité-attribut AKA) sans raison valable, vous exploserez le nombre de lectures et d'écritures physiques.
Si vous ne tirez pas parti des fonctionnalités FILESTREAM
fournies par la base de données, telles que la compression de page, le stockage (pour les données binaires), les SPARSE
colonnes, les hierarchyid
hiérarchies, etc. (tous les exemples SQL Server), vous ne verrez nulle part près de la une performance que vous pourriez voir.
Vous devriez commencer à vous soucier de votre stratégie d'accès aux données après avoir conçu votre base de données et vous être convaincu qu'elle est aussi performante que possible, du moins pour le moment.
Eager vs. Lazy Loading
La plupart des ORM utilisaient une technique appelée " chargement paresseux pour relations", ce qui signifie que par défaut, il charge une entité (ligne de tableau) à la fois et effectue un aller-retour vers la base de données chaque fois qu'elle doit charger un ou plusieurs éléments associés (étrangers). touche) rangées.
Ce n'est pas une bonne ou une mauvaise chose, cela dépend plutôt de ce qui sera réellement fait avec les données et de ce que vous savez en amont. Parfois, le chargement paresseux est absolument la bonne chose à faire. NHibernate, par exemple, peut décider de ne rien interroger du tout et de générer simplement un proxy pour un identifiant particulier. Si tout ce dont vous avez besoin est l’ID lui-même, pourquoi devrait-il en demander plus? D'autre part, si vous essayez d'imprimer une arborescence de chaque élément d'une hiérarchie à 3 niveaux, le chargement différé devient une opération O (N²), ce qui est extrêmement mauvais pour la performance.
L’utilisation de «SQL pur» (c’est-à-dire des requêtes ADO.NET brutes / procédures stockées) présente l’avantage intéressant de vous obliger à déterminer exactement quelles données sont nécessaires pour afficher un écran ou une page donnés. Les ORM et les fonctionnalités de chargement paresseux ne vous empêchent pas de le faire, mais vous donnent la possibilité d'être ... bien, paresseux et d'exploser accidentellement le nombre de requêtes que vous exécutez. Vous devez donc comprendre les fonctionnalités de chargement rapide de vos ORM et être toujours vigilant quant au nombre de requêtes que vous envoyez au serveur pour une requête de page donnée.
Mise en cache
Tous les principaux ORM conservent un cache de premier niveau, AKA "cache d'identité", ce qui signifie que si vous demandez deux fois la même entité par son ID, elle ne nécessite pas de deuxième aller-retour, et également (si vous avez conçu votre base de données correctement). ) vous donne la possibilité d'utiliser une concurrence optimiste.
Le cache L1 est assez opaque dans L2S et EF, il faut en quelque sorte avoir confiance que cela fonctionne. NHibernate est plus explicite à ce sujet ( Get
/ Load
vs. Query
/ QueryOver
). Néanmoins, tant que vous tenterez d’interroger autant que possible par ID, tout devrait bien se passer ici. Beaucoup de gens oublient le cache L1 et cherchent à plusieurs reprises la même entité sous un autre nom que son identifiant (c'est-à-dire un champ de recherche). Si vous avez besoin de le faire, vous devez enregistrer l'ID ou même l'entité entière pour les recherches futures.
Il existe également un cache de niveau 2 ("cache de requête"). NHibernate a cette fonction intégrée. Linq to SQL et Entity Framework ont des requêtes compilées , ce qui peut aider à réduire un peu les charges du serveur d'applications en compilant l'expression de requête elle-même, mais elle ne met pas les données en cache. Microsoft semble considérer cela comme un problème d’application plutôt que comme un problème d’accès aux données, ce qui constitue un point faible majeur à la fois pour L2S et EF. Inutile de dire que c'est aussi un point faible du SQL "brut". Pour obtenir de très bonnes performances avec pratiquement tous les ORM autres que NHibernate, vous devez implémenter votre propre façade de mise en cache.
Il existe également une "extension" de cache L2 pour EF4, ce qui est correct , mais pas vraiment un remplacement en gros pour un cache au niveau de l'application.
Nombre de requêtes
Les bases de données relationnelles sont basées sur des ensembles de données. Ils sont très efficaces pour produire de grandes quantités de données en peu de temps, mais ils sont loin d’être aussi performants en termes de temps d’ attente des requêtes, car chaque commande comporte un certain temps système. Une application bien conçue doit exploiter les atouts de ce SGBD, tenter de minimiser le nombre de requêtes et de maximiser la quantité de données qu'elles contiennent.
Maintenant, je ne dis pas d'interroger la base de données complète lorsque vous n'avez besoin que d'une seule ligne. Ce que je veux dire est, si vous avez besoin Customer
, Address
, Phone
, CreditCard
et des Order
lignes toutes en même temps afin de servir une seule page, alors vous devriez demander à tous en même temps, ne pas exécuter chaque requête séparément. Parfois c'est pire que ça, vous verrez du code qui interroge le même Customer
enregistrement 5 fois de suite, d'abord pour obtenir le Id
, puis le Name
, puis le EmailAddress
, puis ... c'est ridiculement inefficace.
Même si vous devez exécuter plusieurs requêtes qui fonctionnent toutes sur des ensembles de données complètement différents, il est généralement plus efficace de tout envoyer à la base de données sous la forme d'un "script" unique et de lui renvoyer plusieurs ensembles de résultats. Ce qui vous préoccupe, ce sont les frais généraux, et non la quantité totale de données.
Cela peut sembler logique, mais il est souvent très facile de perdre le fil de toutes les requêtes en cours d’exécution dans diverses parties de l’application; votre fournisseur d’appartenance interroge les tables utilisateur / rôle, votre action d’en-tête interroge le panier, votre action Menu interroge la table de plan du site, votre action Barre latérale interroge la liste de produits sélectionnée, puis peut-être que votre page est divisée en plusieurs zones autonomes interrogez séparément les tables Historique des commandes, Récemment consultées, Catégorie et Inventaire, et avant de vous en rendre compte, vous exécutez 20 requêtes avant même de pouvoir commencer à servir la page. Cela détruit complètement les performances.
Certains cadres - et je pense principalement à NHibernate ici - sont extrêmement intelligents à cet égard et vous permettent d’utiliser un système appelé futures qui regroupe des requêtes entières et tente de les exécuter tous à la fois, à la dernière minute possible. Autant que je sache, vous êtes autonome si vous souhaitez le faire avec l’une des technologies Microsoft; vous devez l'intégrer dans votre logique d'application.
Indexation, prédicats et projections
Au moins 50% des développeurs à qui je parle et même certains administrateurs de base de données semblent avoir des problèmes avec le concept d'index de couverture. Ils pensent: "Eh bien, la Customer.Name
colonne est indexée, donc chaque recherche que je fais sur le nom devrait être rapide." Sauf que cela ne fonctionne de cette façon que si l' Name
index couvre la colonne que vous recherchez. Dans SQL Server, cela se fait INCLUDE
dans l' CREATE INDEX
instruction.
Si vous utilisez naïvement SELECT *
partout - et c'est plus ou moins ce que fera chaque ORM à moins d'indication explicite contraire utilisant une projection - alors le SGBD peut très bien choisir d'ignorer complètement vos index car ils contiennent des colonnes non couvertes. Une projection signifie par exemple qu'au lieu de faire ceci:
from c in db.Customers where c.Name == "John Doe" select c
Vous faites cela à la place:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Et cette volonté, pour la plupart des ORM modernes, instruisent à aller seulement et interroger les Id
et Name
colonnes qui sont vraisemblablement couvertes par l'indice (mais pas Email
, LastActivityDate
ou tout autre colonnes se sont passées en tenir là - dedans).
Il est également très facile de supprimer complètement les avantages de l'indexation en utilisant des prédicats inappropriés. Par exemple:
from c in db.Customers where c.Name.Contains("Doe")
... semble presque identique à notre requête précédente, mais aboutira en fait à une analyse complète de la table ou de l'index car elle se traduit par LIKE '%Doe%'
. De même, une autre requête qui semble étrangement simple est:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
En supposant que vous ayez un index BirthDate
, ce prédicat a de bonnes chances de le rendre complètement inutile. Notre programmeur hypothétique ici a évidemment tenté de créer une sorte de requête dynamique ("ne filtrer que la date de naissance si ce paramètre a été spécifié"), mais ce n'est pas la bonne façon de le faire. Écrit comme ceci à la place:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... maintenant le moteur de base de données sait comment paramétrer ceci et faire une recherche d'index. Une modification mineure, apparemment insignifiante, de l'expression de requête peut affecter considérablement les performances.
Malheureusement, en général, LINQ facilite trop l'écriture de mauvaises requêtes, car les fournisseurs sont parfois en mesure de deviner ce que vous avez essayé de faire et d'optimiser la requête, mais ils ne le sont parfois pas. Vous obtenez donc des résultats frustrants et incohérents qui auraient été aveuglément évidents (pour un administrateur de base de données expérimenté, de toute façon) si vous veniez d’écrire du vieux code clair.
En gros, vous devez garder un œil attentif sur le code SQL généré et les plans d’exécution qu’ils génèrent. Si vous n’obtenez pas les résultats escomptés, n’ayez pas peur de contourner le code. Couche ORM de temps en temps et coder manuellement le code SQL. Cela vaut pour n'importe quel ORM, pas seulement EF.
Transactions et verrouillage
Avez-vous besoin d’afficher des données actuelles jusqu’à la milliseconde? Peut-être - ça dépend - mais probablement pas. Malheureusement, Entity Framework ne vous en donne pasnolock
, vous ne pouvez l'utiliser READ UNCOMMITTED
qu'au niveau de la transaction (pas au niveau de la table). En fait, aucun des ORM n'est particulièrement fiable à ce sujet; si vous voulez effectuer des lectures modifiées, vous devez descendre au niveau SQL et écrire des requêtes ad-hoc ou des procédures stockées. Il s’agit donc encore une fois de la facilité avec laquelle vous pouvez le faire dans le cadre.
Entity Framework a parcouru un long chemin à cet égard - la version 1 de EF (dans .NET 3.5) était affreuse, ce qui rendait incroyablement difficile la traversée de l'abstraction des "entités", mais vous avez maintenant ExecuteStoreQuery et Translate , donc c'est vraiment pas mal. Faites-vous des amis avec ces gars parce que vous les utiliserez beaucoup.
Il y a aussi la question du verrouillage en écriture et des blocages, ainsi que de la pratique courante consistant à conserver les verrous dans la base de données le moins longtemps possible. À cet égard, la plupart des ORM (y compris Entity Framework) ont tendance à être meilleurs que le SQL brut, car ils encapsulent le modèle d' unité de travail , qui en EF correspond à SaveChanges . En d'autres termes, vous pouvez "insérer" ou "mettre à jour" ou "supprimer" des entités au contenu de votre coeur, à tout moment, en sachant qu'aucune modification ne sera réellement poussée dans la base de données tant que vous ne aurez pas validé l'unité de travail.
Notez qu'un UOW n'est pas analogue à une transaction longue. L’UOW utilise toujours les fonctionnalités de simultanéité optimistes de l’ORM et suit tous les changements en mémoire . Aucune instruction DML n'est émise avant la validation finale. Cela permet de réduire au maximum les délais de transaction. Si vous construisez votre application en utilisant du SQL brut, il est assez difficile d’obtenir ce comportement différé.
Ce que cela signifie pour EF en particulier: Rendez vos unités de travail aussi grossières que possible et ne les engagez pas avant que vous en ayez absolument besoin. Faites cela et vous vous retrouverez avec des conflits de verrous bien moins importants que si vous utilisiez des commandes ADO.NET individuelles à des moments aléatoires.
EF convient parfaitement aux applications à trafic élevé / hautes performances, tout comme n'importe quel autre framework convient parfaitement aux applications à trafic élevé / hautes performances. Ce qui compte, c'est comment vous l'utilisez. Voici une comparaison rapide des frameworks les plus populaires et de leurs fonctionnalités en termes de performances (légende: N = Non pris en charge, P = Partiel, Y = oui / pris en charge):
Comme vous pouvez le constater, EF4 (la version actuelle) n’est pas trop mal loti, mais ce n’est probablement pas le meilleur choix si les performances sont votre principale préoccupation. NHibernate est beaucoup plus mature dans ce domaine et même Linq to SQL fournit des fonctionnalités améliorant les performances que EF n’a toujours pas. Raw ADO.NET sera souvent plus rapide pour des scénarios d’accès aux données très spécifiques , mais, lorsque vous réunissez tous les éléments, il n’offre vraiment pas les avantages importants que vous retirez des différents frameworks.