Ceci s’applique principalement aux applications asp.net où les données ne sont pas accédées via une architecture orientée services. Cela signifie que vous avez accès aux objets chargés depuis le framework, et non à des objets de transfert, bien que certaines recommandations s’appliquent tout de même.
Ceci est un article communautaire, n’hésitez donc pas à y contribuer comme bon vous semble.
S’applique à : Entity Framework 1.0 livré avec Visual Studio 2008 sp1.
Pourquoi choisir EF en premier lieu ?
Considérant que c’est une technologie jeune avec de nombreux problèmes (voir ci-dessous), il peut être difficile de convaincre d’adopter EF pour votre projet. Cependant, c’est la technologie que Microsoft pousse (aux dépens de Linq2Sql, qui est un sous-ensemble d’EF). De plus, vous n’êtes peut-être pas satisfait de NHibernate ou d’autres solutions existantes. Quelles que soient les raisons, il y a des personnes (moi y compris) qui travaillent avec EF et la vie n’est pas si mauvaise.
EF et l’héritage
Le premier grand sujet est l’héritage. EF prend en charge le mapping pour les classes héritées qui sont persistées de 2 manières : table par classe et table par hiérarchie. La modélisation est facile et il n’y a pas de problèmes de programmation avec cette partie.
(Ce qui suit s’applique au modèle table par classe car je n’ai pas d’expérience avec table par hiérarchie, qui est, de toute façon, limité.) Le vrai problème survient lorsque vous essayez d’exécuter des requêtes incluant un ou plusieurs objets faisant partie d’un arbre d’héritage : le SQL généré est incroyablement horrible, prend beaucoup de temps à être analysé par EF et prend beaucoup de temps à s’exécuter également. C’est un véritable point bloquant. Suffisamment pour qu’EF ne devrait probablement pas être utilisé avec l’héritage, ou aussi peu que possible.
Voici un exemple de la gravité du problème. Mon modèle EF avait environ 30 classes, dont environ 10 faisaient partie d’un arbre d’héritage. En exécutant une requête pour obtenir un élément de la classe de base, quelque chose d’aussi simple que Base.Get(id), le SQL généré faisait plus de 50 000 caractères. Ensuite, lorsque vous essayez de retourner certaines associations, cela dégénère encore plus, allant jusqu’à lancer des exceptions SQL indiquant l’impossibilité de requêter plus de 256 tables à la fois.
D’accord, c’est mauvais. Le concept d’EF est de vous permettre de créer votre structure d’objets sans (ou avec le moins possible de) considération sur l’implémentation réelle de votre table en base de données. Il échoue complètement sur ce point.
Alors, quelles recommandations ? Évitez l’héritage si vous le pouvez, les performances seront bien meilleures. Utilisez-le avec parcimonie lorsque c’est nécessaire. À mon avis, cela fait d’EF un outil glorifié de génération SQL pour les requêtes, mais il y a tout de même des avantages à l’utiliser. Et des moyens d’implémenter des mécanismes similaires à l’héritage.
Contourner l’héritage avec les interfaces
La première chose à savoir lorsque vous essayez d’obtenir une sorte d’héritage avec EF est que vous ne pouvez pas assigner une classe de base non modélisée par EF. N’essayez même pas, le modéliseur l’écrasera. Alors que faire ?
Vous pouvez utiliser des interfaces pour imposer que les classes implémentent certaines fonctionnalités. Par exemple, voici une interface IEntity qui vous permet de définir des associations entre des entités EF lorsque vous ne connaissez pas au moment de la conception quel sera le type de l’entité.
public enum EntityTypes{ Unknown = -1, Dog = 0, Cat }
public interface IEntity
{
int EntityID { get; }
string Name { get; }
Type EntityType { get; }
}
public partial class Dog : IEntity
{
// implement EntityID and Name which could actually be fields
// from your EF model
Type EntityType{ get{ return EntityTypes.Dog; } }
}
En utilisant cette interface IEntity, vous pouvez ensuite travailler avec des associations non définies dans d’autres classes
// lets take a class that you defined in your model.
// that class has a mapping to the columns: PetID, PetType
public partial class Person
{
public IEntity GetPet()
{
return IEntityController.Get(PetID,PetType);
}
}
qui utilise des fonctions d’extension :
public class IEntityController
{
static public IEntity Get(int id, EntityTypes type)
{
switch (type)
{
case EntityTypes.Dog: return Dog.Get(id);
case EntityTypes.Cat: return Cat.Get(id);
default: throw new Exception("Invalid EntityType");
}
}
}
Ce n’est pas aussi élégant qu’un héritage simple, notamment parce que vous devez stocker le PetType dans un champ supplémentaire de la base de données, mais compte tenu des gains de performance, je ne reviendrais pas en arrière.
Cela ne peut pas non plus modéliser des relations un-à-plusieurs ou plusieurs-à-plusieurs, mais avec des utilisations créatives de ‘Union’, cela pourrait fonctionner. Enfin, cela crée l’effet secondaire de charger des données dans une propriété/fonction de l’objet, ce qui nécessite de la prudence. Utiliser une convention de nommage claire comme GetXYZ() aide à cet égard.
Requêtes compilées
Les performances d’Entity Framework ne sont pas aussi bonnes qu’un accès direct à la base de données avec ADO (évidemment) ou Linq2SQL. Il existe cependant des moyens de les améliorer, dont l’un est de compiler vos requêtes. Les performances d’une requête compilée sont similaires à Linq2Sql.
Qu’est-ce qu’une requête compilée ? C’est simplement une requête pour laquelle vous indiquez au framework de conserver l’arbre analysé en mémoire afin qu’il n’ait pas besoin d’être régénéré lors de la prochaine exécution. Ainsi, lors de la prochaine exécution, vous économiserez le temps nécessaire à l’analyse de l’arbre. Ne sous-estimez pas cela car c’est une opération très coûteuse qui s’aggrave avec des requêtes plus complexes.
Il existe 2 façons de compiler une requête : créer un ObjectQuery avec EntitySQL et utiliser la fonction CompiledQuery.Compile(). (Notez qu’en utilisant un EntityDataSource dans votre page, vous utilisez en fait ObjectQuery avec EntitySQL, ce qui est compilé et mis en cache).
Un aparté ici au cas où vous ne sauriez pas ce qu’est EntitySQL. C’est une manière d’écrire des requêtes sous forme de chaînes de caractères contre EF. Voici un exemple : “select value dog from Entities.DogSet as dog where dog.ID = @ID”. La syntaxe est assez similaire à celle de SQL. Vous pouvez également effectuer des manipulations d’objets assez complexes, ce qui est bien expliqué [ici][1].
D’accord, voici comment procéder en utilisant ObjectQuery<>
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance));
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
La première fois que vous exécutez cette requête, le framework génère l’arbre d’expression et le garde en mémoire. Ainsi, la prochaine fois qu’elle sera exécutée, vous économiserez cette étape coûteuse. Dans cet exemple, EnablePlanCaching = true, ce qui est inutile puisque c’est l’option par défaut.
L’autre façon de compiler une requête pour une utilisation ultérieure est la méthode CompiledQuery.Compile. Celle-ci utilise un délégué :
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
ctx.DogSet.FirstOrDefault(it => it.ID == id));
ou en utilisant LINQ
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
(from dog in ctx.DogSet where dog.ID == id select dog).FirstOrDefault());
pour appeler la requête :
query_GetDog.Invoke( YourContext, id );
L’avantage de CompiledQuery est que la syntaxe de votre requête est vérifiée au moment de la compilation, contrairement à EntitySQL. Cependant, il y a d’autres considérations…
Includes
Disons que vous souhaitez que les données du propriétaire du chien soient retournées par la requête pour éviter de faire 2 appels à la base de données. Facile à faire, n’est-ce pas ?
EntitySQL
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance)).Include("Owner");
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
CompiledQuery
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
(from dog in ctx.DogSet.Include("Owner") where dog.ID == id select dog).FirstOrDefault());
Maintenant, que faire si vous voulez paramétrer l’Include ? Ce que je veux dire, c’est que vous souhaitez avoir une seule fonction Get() qui est appelée depuis différentes pages qui se soucient de différentes relations pour le chien. L’une se soucie du propriétaire, une autre de sa nourriture préférée, une autre de son jouet préféré, etc. En gros, vous voulez indiquer à la requête quelles associations charger.
C’est facile à faire avec EntitySQL
public Dog Get(int id, string include)
{
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance))
.IncludeMany(include);
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
}
L’include utilise simplement la chaîne passée. Assez simple. Notez qu’il est possible d’améliorer la fonction Include(string) (qui n’accepte qu’un seul chemin) avec un IncludeMany(string) qui vous permettra de passer une chaîne d’associations séparées par des virgules à charger. Consultez la section extensions plus loin pour cette fonction.
Si nous essayons de faire cela avec CompiledQuery cependant, nous rencontrons de nombreux problèmes :
L’approche évidente
static readonly Func<Entities, int, string, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
(from dog in ctx.DogSet.Include(include) where dog.ID == id select dog).FirstOrDefault());
va échouer lorsqu’elle est appelée avec :
query_GetDog.Invoke( YourContext, id, "Owner,FavoriteFood" );
Parce que, comme mentionné ci-dessus, Include() ne veut voir qu’un seul chemin dans la chaîne et ici nous lui en donnons 2 : “Owner” et “FavoriteFood” (ce qui ne doit pas être confondu avec “Owner.FavoriteFood” !).
Alors, utilisons IncludeMany(), qui est une fonction d’extension
static readonly Func<Entities, int, string, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
(from dog in ctx.DogSet.IncludeMany(include) where dog.ID == id select dog).FirstOrDefault());
Encore faux, cette fois c’est parce qu’EF ne peut pas analyser IncludeMany car elle ne fait pas partie des fonctions qu’il reconnaît : c’est une extension.
D’accord, vous voulez donc passer un nombre arbitraire de chemins à votre fonction et Include() n’en prend qu’un seul. Que faire ? Vous pourriez décider que vous n’aurez jamais besoin de plus de, disons, 20 Includes, et passer chaque chaîne séparée dans une structure à CompiledQuery. Mais maintenant la requête ressemble à ceci :
from dog in ctx.DogSet.Include(include1).Include(include2).Include(include3)
.Include(include4).Include(include5).Include(include6)
.[...].Include(include19).Include(include20) where dog.ID == id select dog
ce qui est horrible aussi. D’accord, mais attendez une minute. Ne pouvons-nous pas retourner un ObjectQuery<> avec CompiledQuery ? Puis définir les includes dessus ? Eh bien, c’est aussi ce que j’aurais pensé :
static readonly Func<Entities, int, ObjectQuery<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, ObjectQuery<Dog>>((ctx, id) =>
(ObjectQuery<Dog>)(from dog in ctx.DogSet where dog.ID == id select dog));
public Dog GetDog( int id, string include )
{
ObjectQuery<Dog> oQuery = query_GetDog(id);
oQuery = oQuery.IncludeMany(include);
return oQuery.FirstOrDefault;
}
Cela aurait dû fonctionner, sauf que lorsque vous appelez IncludeMany (ou Include, Where, OrderBy…) vous invalidez la requête compilée mise en cache car c’est une requête entièrement nouvelle maintenant ! L’arbre d’expression doit donc être ré-analysé et vous subissez à nouveau cette perte de performances.
Alors, quelle est la solution ? Vous ne pouvez tout simplement pas utiliser les requêtes compilées avec des Includes paramétrés. Utilisez EntitySQL à la place. Cela ne signifie pas que les requêtes compilées n’ont pas d’utilité. C’est excellent pour des requêtes localisées qui seront toujours appelées dans le même contexte. Idéalement, CompiledQuery devrait toujours être utilisé car la syntaxe est vérifiée au moment de la compilation, mais en raison de cette limitation, ce n’est pas possible.
Un exemple d’utilisation serait : vous pourriez avoir une page qui recherche quels deux chiens ont la même nourriture préférée, ce qui est un peu trop spécifique pour une fonction de couche métier, donc vous la mettez dans votre page et savez exactement quels types d’includes sont nécessaires.
Passer plus de 3 paramètres à une CompiledQuery
Func est limité à 5 paramètres, dont le dernier est le type de retour et le premier est votre objet Entities du modèle. Il vous reste donc 3 paramètres. Une misère, mais cela peut être amélioré très facilement.
public struct MyParams
{
public string param1;
public int param2;
public DateTime param3;
}
static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
from dog in ctx.DogSet where dog.Age == myParams.param2 && dog.Name == myParams.param1 and dog.BirthDate > myParams.param3 select dog);
public List<Dog> GetSomeDogs( int age, string Name, DateTime birthDate )
{
MyParams myParams = new MyParams();
myParams.param1 = name;
myParams.param2 = age;
myParams.param3 = birthDate;
return query_GetDog(YourContext,myParams).ToList();
}
Types de retour (ceci ne s’applique pas aux requêtes EntitySQL car elles ne sont pas compilées au même moment pendant l’exécution que la méthode CompiledQuery)
En travaillant avec LINQ, vous ne forcez généralement pas l’exécution de la requête jusqu’au tout dernier moment, au cas où une autre fonction en aval voudrait modifier la requête d’une certaine manière :
static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);
public IEnumerable<Dog> GetSomeDogs( int age, string name )
{
return query_GetDog(YourContext,age,name);
}
public void DataBindStuff()
{
IEnumerable<Dog> dogs = GetSomeDogs(4,"Bud");
// but I want the dogs ordered by BirthDate
gridView.DataSource = dogs.OrderBy( it => it.BirthDate );
}
Que va-t-il se passer ici ? En continuant à manipuler l’ObjectQuery original (qui est le type de retour réel de l’instruction LINQ, qui implémente IEnumerable), cela va invalider la requête compilée et forcer une ré-analyse. Donc, la règle générale est de retourner une List<> d’objets à la place.
static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);
public List<Dog> GetSomeDogs( int age, string name )
{
return query_GetDog(YourContext,age,name).ToList(); //<== change here
}
public void DataBindStuff()
{
List<Dog> dogs = GetSomeDogs(4,"Bud");
// but I want the dogs ordered by BirthDate
gridView.DataSource = dogs.OrderBy( it => it.BirthDate );
}
Lorsque vous appelez ToList(), la requête est exécutée selon la requête compilée et ensuite, plus tard, le OrderBy est exécuté contre les objets en mémoire. Cela peut être un tout petit peu plus lent, mais je n’en suis même pas sûr. Ce qui est certain, c’est que vous n’avez aucun souci concernant la mauvaise manipulation de l’ObjectQuery et l’invalidation du plan de requête compilé.
Encore une fois, ce n’est pas une règle absolue. ToList() est une astuce de programmation défensive, mais si vous avez une raison valable de ne pas utiliser ToList(), allez-y. Il y a de nombreux cas où vous voudriez affiner la requête avant de l’exécuter.
Performance
Quel est l’impact sur les performances de la compilation d’une requête ? Il peut en fait être assez important. En règle générale, compiler et mettre en cache la requête pour réutilisation prend au moins le double du temps de sa simple exécution sans mise en cache. Pour des requêtes complexes (avec héritage), j’ai vu des temps allant jusqu’à 10 secondes.
Donc, la première fois qu’une requête pré-compilée est appelée, vous subissez un impact sur les performances. Après ce premier impact, les performances sont nettement meilleures que la même requête non pré-compilée. Pratiquement les mêmes que Linq2Sql.
Lorsque vous chargez une page avec des requêtes pré-compilées pour la première fois, vous subissez un impact. Elle se chargera en peut-être 5-15 secondes (évidemment plus d’une requête pré-compilée finira par être appelée), tandis que les chargements suivants prendront moins de 300ms. Une différence dramatique, et c’est à vous de décider si c’est acceptable que votre premier utilisateur subisse cet impact ou si vous voulez un script qui appelle vos pages pour forcer la compilation des requêtes.
Cette requête peut-elle être mise en cache ?
{
Dog dog = from dog in YourContext.DogSet where dog.ID == id select dog;
}
Non, les requêtes LINQ ad-hoc ne sont pas mises en cache et vous subirez le coût de génération de l’arbre à chaque fois que vous l’appelez.
Requêtes paramétrées
La plupart des fonctionnalités de recherche impliquent des requêtes fortement paramétrées. Il existe même des bibliothèques qui vous permettent de construire une requête paramétrée à partir d’expressions lambda. Le problème est que vous ne pouvez pas utiliser de requêtes pré-compilées avec celles-ci. Un moyen de contourner cela est de cartographier tous les critères possibles dans la requête et d’indiquer lesquels vous souhaitez utiliser :
public struct MyParams
{
public string name;
public bool checkName;
public int age;
public bool checkAge;
}
static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
from dog in ctx.DogSet
where (myParams.checkAge == true && dog.Age == myParams.age)
&& (myParams.checkName == true && dog.Name == myParams.name )
select dog);
protected List<Dog> GetSomeDogs()
{
MyParams myParams = new MyParams();
myParams.name = "Bud";
myParams.checkName = true;
myParams.age = 0;
myParams.checkAge = false;
return query_GetDog(YourContext,myParams).ToList();
}
L’avantage ici est que vous obtenez tous les bénéfices d’une requête pré-compilée. Les inconvénients sont que vous finirez très probablement avec une clause WHERE assez difficile à maintenir, que vous subirez une pénalité plus importante pour la pré-compilation de la requête et que chaque requête exécutée n’est pas aussi efficace qu’elle pourrait l’être (particulièrement avec des jointures).
Une autre façon est de construire une requête EntitySQL morceau par morceau, comme nous le faisions tous avec SQL.
protected List<Dod> GetSomeDogs( string name, int age)
{
string query = "select value dog from Entities.DogSet where 1 = 1 ";
if( !String.IsNullOrEmpty(name) )
query = query + " and dog.Name == @Name ";
if( age > 0 )
query = query + " and dog.Age == @Age ";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
if( !String.IsNullOrEmpty(name) )
oQuery.Parameters.Add( new ObjectParameter( "Name", name ) );
if( age > 0 )
oQuery.Parameters.Add( new ObjectParameter( "Age", age ) );
return oQuery.ToList();
}
Ici les problèmes sont :
- il n’y a pas de vérification syntaxique durant la compilation
- chaque combinaison différente de paramètres génère une requête différente qui devra être pré-compilée lors de sa première exécution. Dans ce cas, il n’y a que 4 requêtes possibles différentes (pas de paramètres, âge seulement, nom seulement et les deux paramètres), mais vous pouvez voir qu’il peut y en avoir bien plus avec une recherche normale.
- Personne n’aime concaténer des chaînes !
Une autre option est d’interroger un grand sous-ensemble des données puis de les affiner en mémoire. C’est particulièrement utile si vous travaillez avec un sous-ensemble défini des données, comme tous les chiens d’une ville. Vous savez qu’il y en a beaucoup mais vous savez aussi qu’il n’y en a pas tant que ça… donc votre page de recherche CityDog peut charger tous les chiens de la ville en mémoire, ce qui est une seule requête pré-compilée, puis affiner les résultats
protected List<Dod> GetSomeDogs( string name, int age, string city)
{
string query = "select value dog from Entities.DogSet where dog.Owner.Address.City == @City ";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
oQuery.Parameters.Add( new ObjectParameter( "City", city ) );
List<Dog> dogs = oQuery.ToList();
if( !String.IsNullOrEmpty(name) )
dogs = dogs.Where( it => it.Name == name );
if( age > 0 )
dogs = dogs.Where( it => it.Age == age );
return dogs;
}
C’est particulièrement utile lorsque vous commencez par afficher toutes les données puis permettez le filtrage.
Problèmes :
- Pourrait entraîner un transfert de données important si vous ne faites pas attention à votre sous-ensemble.
- Vous ne pouvez filtrer que sur les données que vous avez retournées. Cela signifie que si vous ne retournez pas l’association Dog.Owner, vous ne pourrez pas filtrer sur Dog.Owner.Name
Alors, quelle est la meilleure solution ? Il n’y en a pas. Vous devez choisir la solution qui fonctionne le mieux pour vous et votre problème :
- Utilisez la construction de requêtes basée sur les lambdas lorsque vous ne vous souciez pas de la pré-compilation de vos requêtes.
- Utilisez des requêtes LINQ pré-compilées entièrement définies lorsque la structure de vos objets n’est pas trop complexe.
- Utilisez EntitySQL/concaténation de chaînes lorsque la structure pourrait être complexe et lorsque le nombre possible de requêtes résultantes différentes est faible (ce qui signifie moins d’impacts de pré-compilation).
- Utilisez le filtrage en mémoire lorsque vous travaillez avec un petit sous-ensemble de données ou lorsque vous deviez de toute façon récupérer toutes les données au départ (si les performances sont bonnes avec toutes les données, alors le filtrage en mémoire ne causera pas de temps supplémentaire en base de données).
Accès Singleton
La meilleure façon de gérer votre contexte et vos entités à travers toutes vos pages est d’utiliser le patron Singleton :
public sealed class YourContext
{
private const string instanceKey = "On3GoModelKey";
YourContext(){}
public static YourEntities Instance
{
get
{
HttpContext context = HttpContext.Current;
if( context == null )
return Nested.instance;
if (context.Items[instanceKey] == null)
{
On3GoEntities entity = new On3GoEntities();
context.Items[instanceKey] = entity;
}
return (YourEntities)context.Items[instanceKey];
}
}
class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly YourEntities instance = new YourEntities();
}
}
NoTracking, est-ce que cela en vaut la peine ?
Lorsque vous exécutez une requête, vous pouvez indiquer au framework de suivre ou non les objets qu’il retournera. Qu’est-ce que cela signifie ? Avec le suivi activé (l’option par défaut), le framework suivra ce qui se passe avec l’objet (a-t-il été modifié ? Créé ? Supprimé ?) et liera également les objets entre eux, lorsque d’autres requêtes sont faites depuis la base de données, ce qui est le point qui nous intéresse ici.
Par exemple, supposons que le chien avec ID == 2 a un propriétaire dont l’ID == 10.
Dog dog = (from dog in YourContext.DogSet where dog.ID == 2 select dog).FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
Person owner = (from o in YourContext.PersonSet where o.ID == 10 select dog).FirstOrDefault();
//dog.OwnerReference.IsLoaded == true;
Si nous faisions la même chose sans suivi, le résultat serait différent.
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
(from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog = oDogQuery.FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)
(from o in YourContext.PersonSet where o.ID == 10 select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
Owner owner = oPersonQuery.FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
Le suivi est très utile et dans un monde parfait sans problème de performance, il serait toujours activé. Mais dans ce monde, il y a un prix à payer en termes de performance. Alors, devriez-vous utiliser NoTracking pour accélérer les choses ? Cela dépend de l’utilisation que vous prévoyez pour les données.
Y a-t-il une chance que les données que vous interrogez avec NoTracking soient utilisées pour faire des mises à jour/insertions/suppressions dans la base de données ? Si oui, n’utilisez pas NoTracking car les associations ne sont pas suivies et causeront des exceptions.
Dans une page où il n’y a absolument aucune mise à jour de la base de données, vous pouvez utiliser NoTracking.
Mélanger le suivi et le NoTracking est possible, mais cela nécessite d’être extrêmement prudent avec les mises à jour/insertions/suppressions. Le problème est que si vous mélangez les deux, vous risquez que le framework essaie de faire un Attach() d’un objet NoTracking au contexte où une autre copie du même objet existe avec le suivi activé. En gros, ce que je dis c’est que
Dog dog1 = (from dog in YourContext.DogSet where dog.ID == 2).FirstOrDefault();
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
(from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog2 = oDogQuery.FirstOrDefault();
dog1 et dog2 sont 2 objets différents, l’un suivi et l’autre non. Utiliser l’objet détaché dans une mise à jour/insertion forcera un Attach() qui dira “Attendez une minute, j’ai déjà un objet ici avec la même clé de base de données. Échec”. Et lorsque vous faites Attach() sur un objet, toute sa hiérarchie est attachée également, causant des problèmes partout. Soyez extrêmement prudent.
Quelle est la différence de vitesse avec NoTracking ?
Cela dépend des requêtes. Certaines sont bien plus sensibles au suivi que d’autres. Je n’ai pas de règle rapide et facile pour cela, mais ça aide.
Alors je devrais utiliser NoTracking partout ?
Pas exactement. Il y a certains avantages au suivi des objets. Le premier est que l’objet est mis en cache, donc les appels suivants pour cet objet ne toucheront pas la base de données. Ce cache n’est valide que pour la durée de vie de l’objet YourEntities, qui, si vous utilisez le code Singleton ci-dessus, est la même que la durée de vie de la page. Une requête de page == un objet YourEntity. Donc, pour des appels multiples pour le même objet, il ne sera chargé qu’une seule fois par requête de page. (D’autres mécanismes de mise en cache pourraient étendre cela).
Que se passe-t-il lorsque vous utilisez NoTracking et essayez de charger le même objet plusieurs fois ? La base de données sera interrogée à chaque fois, il y a donc un impact. À quelle fréquence devriez-vous/appelez-vous le même objet durant une seule requête de page ? Le moins possible bien sûr, mais cela arrive.
Souvenez-vous également du passage ci-dessus concernant la connexion automatique des associations ? Vous n’avez pas cela avec NoTracking, donc si vous chargez vos données en plusieurs lots, vous n’aurez pas de lien entre eux :
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)(from dog in YourContext.DogSet select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
List<Dog> dogs = oDogQuery.ToList();
ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)(from o in YourContext.PersonSet select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
List<Person> owners = oPersonQuery.ToList();
Dans ce cas, aucun chien n’aura sa propriété .Owner définie.
Quelques éléments à garder en tête lorsque vous essayez d’optimiser les performances.
Pas de chargement différé, que faire ?
Cela peut être considéré comme une bénédiction déguisée. Bien sûr, c’est ennuyeux de tout charger manuellement. Cependant, cela réduit le nombre d’appels à la base de données et vous force à réfléchir à quand vous devriez charger les données. Plus vous pouvez charger en un seul appel à la base de données, mieux c’est. C’était toujours vrai, mais c’est maintenant imposé avec cette ‘fonctionnalité’ d’EF.
Bien sûr, vous pouvez appeler
if( !ObjectReference.IsLoaded ) ObjectReference.Load();
si vous le souhaitez, mais une meilleure pratique est de forcer le framework à charger les objets dont vous savez avoir besoin en une seule fois. C’est là que la discussion sur les Includes paramétrés commence à prendre tout son sens.
Disons que vous avez votre objet Dog
public class Dog
{
public Dog Get(int id)
{
return YourContext.DogSet.FirstOrDefault(it => it.ID == id );
}
}
C’est le type de fonction avec laquelle vous travaillez tout le temps. Elle est appelée de partout et une fois que vous avez cet objet Dog, vous en ferez des choses très différentes dans différentes fonctions. Premièrement, elle devrait être pré-compilée, car vous l’appellerez très souvent. Deuxièmement, chaque page différente voudra avoir accès à un sous-ensemble différent des données du Dog. Certaines voudront le propriétaire, d’autres le jouet préféré, etc.
Bien sûr, vous pourriez appeler Load() pour chaque référence dont vous avez besoin chaque fois que vous en avez besoin. Mais cela générera un appel à la base de données à chaque fois. Mauvaise idée. Donc à la place, chaque page demandera les données qu’elle veut voir lorsqu’elle demande l’objet Dog pour la première fois :
static public Dog Get(int id) { return GetDog(entity,"");}
static public Dog Get(int id, string includePath)
{
string query = "select value o " +
" from YourEntities.DogSet as o " +