64 votes

Quelles sont les bonnes pratiques de conception lorsqu'on travaille avec Entity Framework ?

Cela s'applique principalement à une application asp.net où les données ne sont pas accessibles via Soa. Cela signifie que vous avez accès aux objets chargés depuis le framework, et non aux objets de transfert, bien que certaines recommandations s'appliquent toujours.

Il s'agit d'un poste communautaire, alors n'hésitez pas à le compléter comme bon vous semble.

S'applique à : Entity Framework 1.0 livré avec Visual Studio 2008 sp1.

Pourquoi choisir EF en premier lieu ?

Étant donné qu'il s'agit d'une technologie jeune qui présente de nombreux problèmes (voir ci-dessous), il peut être difficile d'adhérer à l'EF pour votre projet. Cependant, c'est la technologie que Microsoft met en avant (au détriment de Linq2Sql, qui est un sous-ensemble de EF). En outre, vous n'êtes peut-être pas satisfait de NHibernate ou d'autres solutions existantes. Quelles que soient les raisons, il y a des gens (dont moi) qui travaillent avec EF et la vie n'est pas mal.vous faire réfléchir.

EF et héritage

Le premier grand sujet est l'héritage. EF supporte le mappage des classes héritées qui sont persistées de 2 façons : 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 de table par classe car je n'ai pas d'expérience avec la table par hiérarchie, qui est de toute façon limitée). Le vrai problème survient lorsque vous essayez d'exécuter des requêtes qui incluent 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 l'EF et prend également beaucoup de temps à s'exécuter. C'est un véritable casse-tête. C'est suffisant pour que EF ne soit pas utilisé avec de l'héritage ou le moins possible.

Voici un exemple de l'état des choses. Mon modèle EF comportait ~30 classes, dont ~10 faisaient partie d'un arbre d'héritage. En exécutant une requête pour obtenir un élément de la classe Base, quelque chose d'aussi simple que Base.Get(id), le SQL généré était de plus de 50 000 caractères. Ensuite, lorsque vous essayez de renvoyer des associations, cela dégénère encore plus, allant jusqu'à lancer des exceptions SQL pour ne pas être capable d'interroger plus de 256 tables à la fois.

Le concept d'EF est de vous permettre de créer votre structure d'objet sans (ou avec le moins de considération possible) sur l'implémentation réelle de votre table dans la base de données. Il échoue complètement à ce niveau.

Alors, des recommandations ? Évitez l'héritage si vous le pouvez, les performances seront tellement meilleures. Utilisez-le avec parcimonie lorsque vous devez le faire. À mon avis, cela fait de EF un outil de génération de sql glorifié pour les requêtes, mais il y a toujours des avantages à l'utiliser. Et des moyens d'implémenter le mécanisme qui sont similaires à l'héritage.

Contourner l'héritage avec les interfaces

La première chose à savoir pour essayer de mettre en place une sorte d'héritage avec EF est que vous ne pouvez pas assigner une classe non modélisée par EF comme classe de base. N'essayez même pas, elle sera écrasée par le modeleur. Alors, que faire ?

Vous pouvez utiliser des interfaces pour obliger les classes à mettre en œuvre certaines fonctionnalités. Par exemple, voici une interface IEntity qui vous permet de définir des associations entre des entités EF dont vous ne connaissez pas le type au moment de la conception.

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 IEntity, vous pouvez ensuite travailler avec des associations indé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 fait appel à certaines 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 simple qu'un simple héritage, surtout si l'on considère qu'il faut stocker le type d'animal 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.

Il ne peut pas non plus modéliser les relations de type "un à plusieurs" ou "plusieurs à plusieurs", mais il est possible de les faire fonctionner en utilisant de manière créative le terme "Union". Enfin, cela crée l'effet secondaire de charger des données dans une propriété/fonction de l'objet, ce à quoi il faut faire attention. L'utilisation d'une convention de dénomination claire comme GetXYZ() est utile à cet égard.

Requêtes compilées

Les performances d'Entity Framework ne sont pas aussi bonnes que celles de l'accès direct aux bases de données avec ADO (évidemment) ou Linq2SQL. Il existe cependant des moyens de les améliorer, l'un d'entre eux étant la compilation de vos requêtes. Les performances d'une requête compilée sont similaires à celles de Linq2Sql.

Qu'est-ce qu'une requête compilée ? Il s'agit simplement d'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 négligez pas cet aspect, car il s'agit d'une opération très coûteuse qui s'aggrave avec les requêtes plus complexes.

Il y a 2 façons de compiler une requête : créer un ObjectQuery avec EntitySQL et utiliser la fonction CompiledQuery.Compile(). (Notez qu'en utilisant une EntityDataSource dans votre page, vous utiliserez en fait un ObjectQuery avec EntitySQL, qui sera donc compilé et mis en cache).

Une parenthèse ici, au cas où vous ne sauriez pas ce qu'est EntitySQL. Il s'agit d'un moyen d'écrire des requêtes sur l'EF à partir de chaînes de caractères. Voici un exemple : "select value dog from Entities.DogSet as dog where dog.ID = @ID". La syntaxe est assez similaire à la syntaxe SQL. Vous pouvez aussi faire des manipulations d'objets assez complexes, ce qui est bien expliqué [ici][1].

Ok, alors voici comment le faire 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 va générer l'arbre d'expression et le garder 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 moyen de compiler une requête pour une utilisation ultérieure est la méthode CompiledQuery.Compile. Elle 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, alors qu'EntitySQL ne l'est pas. Cependant, il y a d'autres considérations...

Comprend

Supposons que vous vouliez que les données relatives au propriétaire du chien soient renvoyées par la requête pour éviter de faire deux appels à la base de données. Facile à faire, non ?

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 se passe-t-il si vous voulez que l'inclusion soit paramétrée ? Ce que je veux dire, c'est que vous voulez avoir une seule fonction Get() qui est appelée depuis différentes pages qui s'intéressent à différentes relations pour le chien. L'une s'intéresse au propriétaire, une autre à sa nourriture préférée, une autre à son jouet préféré, etc. En gros, vous voulez indiquer à la requête les 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. C'est assez simple. Notez qu'il est possible d'améliorer la fonction Include(string) (qui n'accepte qu'un seul chemin) avec une IncludeMany(string) qui vous permettra de passer une chaîne d'associations séparées par des virgules à charger. Regardez plus loin dans la section extension pour cette fonction.

Cependant, si nous essayons de le faire avec CompiledQuery, nous rencontrons de nombreux problèmes :

L'évidence

    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());

s'étouffera lorsqu'il sera appelé 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" (à ne pas confondre avec "Owner.FavoriteFood" !).

Ensuite, 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 que l'EF ne peut pas analyser IncludeMany car il ne fait pas partie des fonctions reconnues : c'est une extension.

Ok, donc vous voulez passer un nombre arbitraire de chemins à votre fonction et Includes() 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

qui est aussi horrible. Ok, alors, mais attendez une minute. Ne pouvons-nous pas retourner un ObjectQuery<> avec CompiledQuery ? Puis définir les inclusions sur cela ? Eh bien, c'est ce que j'aurais pensé aussi :

    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 il s'agit maintenant d'une requête entièrement nouvelle ! L'arbre d'expression doit donc être réparé et les performances sont à nouveau affectées.

Quelle est donc la solution ? Vous ne pouvez tout simplement pas utiliser les CompiledQueries avec des Includes paramétrés. Utilisez plutôt EntitySQL. Cela ne veut pas dire que les CompiledQueries n'ont pas d'utilité. Il est excellent pour les 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 des limitations, ce n'est pas possible.

Voici un exemple d'utilisation : vous pouvez avoir une page qui demande quels sont les deux chiens qui ont le même plat préféré, ce qui est un peu étroit pour une fonction BusinessLayer, vous l'insérez donc dans votre page et vous savez exactement quel type d'inclusion est nécessaire.

Passage de 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 ne vous reste donc que 3 paramètres. Une pitance, mais qui peut être améliorée 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 en même temps lors de 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 manière ou d'une autre :

    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 jouant encore avec l'ObjectQuery original (c'est-à-dire le type de retour réel de l'instruction Linq, qui implémente IEnumerable), la requête compilée sera invalidée et il faudra la reparamétrer. Donc, la règle générale est de retourner une liste<> 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 conformément à la requête compilée, puis, plus tard, la commande OrderBy est exécutée sur les objets en mémoire. Cela peut être un peu plus lent, mais je n'en suis pas sûr. Ce qui est sûr, c'est que vous n'avez pas à craindre de mal gérer l'ObjectQuery et d'invalider le plan de requête compilé.

Une fois encore, il ne s'agit pas d'une déclaration générale. ToList() est une astuce de programmation défensive, mais si vous avez une raison valable de ne pas utiliser ToList(), allez-y. Il existe de nombreux cas dans lesquels vous souhaitez affiner la requête avant de l'exécuter.

Performance

Quel est l'impact de la compilation d'une requête sur les performances ? Il peut en fait être assez important. En règle générale, la compilation et la mise en cache de la requête en vue de sa réutilisation prennent au moins deux fois plus de temps que sa simple exécution sans mise en cache. Pour les requêtes complexes (lire "héréditaires"), j'ai constaté que le temps d'exécution pouvait atteindre 10 secondes.

Ainsi, la première fois qu'une requête précompilée est appelée, les performances sont affectées. Après ce premier échec, les performances sont nettement meilleures que celles de la même requête non précompilée. Pratiquement la même chose que Linq2Sql.

Lorsque vous chargez une page avec des requêtes précompilées la première fois, vous obtenez un résultat. Le chargement prendra peut-être 5 à 15 secondes (il est évident que plus d'une requête précompilée sera appelée), tandis que les chargements suivants prendront moins de 300 ms. La différence est considérable, et c'est à vous de décider s'il est acceptable que votre premier utilisateur soit touché ou si vous voulez qu'un script appelle vos pages pour forcer une 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 devrez supporter le coût de la génération de l'arbre à chaque fois que vous l'appellerez.

Requêtes paramétrées

La plupart des capacité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 lamba. Le problème est que vous ne pouvez pas utiliser de requêtes précompilées avec ces bibliothèques. Une façon de contourner ce problème est de définir tous les critères possibles dans la requête et de marquer celui que vous voulez 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 est que vous bénéficiez de tous les avantages d'un quert pré-compilé. Les inconvénients sont que vous vous retrouverez très probablement avec une clause where difficile à maintenir, que vous subirez une pénalité plus importante pour avoir pré-compilé la requête et que chaque requête exécutée ne sera pas aussi efficace qu'elle pourrait l'être (en particulier avec les jointures).

Une autre méthode consiste à construire une requête EntitySQL morceau par morceau, comme nous l'avons tous fait 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();
}

Les problèmes sont les suivants - il n'y a pas de contrôle syntaxique pendant 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 différentes possibles (pas de paramètres, âge seulement, nom seulement et les deux paramètres), mais vous pouvez voir qu'il peut y en avoir beaucoup plus avec une recherche mondiale normale. - Personne n'aime concaténer des chaînes de caractères !

Une autre option consiste à interroger un grand sous-ensemble de données, puis à le réduire en mémoire. C'est particulièrement utile si vous travaillez avec un sous-ensemble défini de 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... votre page de recherche CityDog peut donc charger tous les chiens de la ville en mémoire, ce qui constitue 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;
}

Il est particulièrement utile lorsque vous commencez à afficher toutes les données puis que vous permettez le filtrage.

Problèmes : - Peut 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 renvoyez pas l'association Dog.Owner, vous ne pourrez pas filtrer sur Dog.Owner.Name. Quelle est donc 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 lambda 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 peut être complexe et lorsque le nombre possible de requêtes différentes est faible (ce qui signifie moins d'occurrences 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 aller chercher toutes les données au départ (si les performances sont bonnes avec toutes les données, le filtrage en mémoire ne fera pas perdre de temps à la base de données).

Accès singleton

La meilleure façon de gérer votre contexte et vos entités sur toutes vos pages est d'utiliser le modèle 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 ça vaut le coup ?

Lors de l'exécution d'une requête, vous pouvez indiquer au framework de suivre les objets qu'il retournera ou non. Qu'est-ce que cela signifie ? Si le suivi est activé (option par défaut), le framework suivra ce qui se passe avec l'objet (a-t-il été modifié ? créé ? supprimé ?) et reliera également les objets entre eux, lorsque d'autres requêtes seront effectuées à partir de la base de données, ce qui nous intéresse ici.

Par exemple, supposons que le chien dont l'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 performances. Alors, faut-il utiliser NoTracking pour accélérer les choses ? Cela dépend de l'utilisation que vous comptez faire des données.

Y a-t-il une chance que les données que vous interrogez avec NoTracking puissent être utilisées pour faire des mises à jour/insertions/suppressions dans la base de données ? Si c'est le cas, n'utilisez pas NoTracking car les associations ne sont pas suivies et cela entraînera la levée d'exceptions.

Dans une page où il n'y a absolument aucune mise à jour de la base de données, vous pouvez utiliser NoTracking.

Il est possible de combiner le suivi et le NoTracking, mais il faut faire très attention aux mises à jour/insertions/suppressions. Le problème est que si vous mélangez, vous risquez de voir le framework essayer d'attacher() un objet NoTracking au contexte où une autre copie du même objet existe avec le suivi activé. En gros, ce que je veux dire, 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 détaché et l'autre non. L'utilisation de 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 quand vous attachez un objet, toute sa hiérarchie est également attachée, ce qui cause des problèmes partout. Soyez très prudent.

Combien de temps cela prend-il avec NoTracking ?

Cela dépend des requêtes. Certaines sont beaucoup plus susceptibles d'être suivies que d'autres. Je n'ai pas de règle simple et rapide pour cela, mais cela aide.

Je devrais donc utiliser NoTracking partout, alors ?

Pas exactement. Il y a quelques avantages à suivre un objet. Le premier est que l'objet est mis en cache, de sorte que les appels ultérieurs pour cet objet ne toucheront pas la base de données. Ce cache n'est valable 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 demande de page == un objet YourEntities. Ainsi, en cas d'appels multiples pour le même objet, il ne sera chargé qu'une seule fois par demande de page. (Un autre mécanisme de mise en cache pourrait étendre cela).

Que se passe-t-il lorsque vous utilisez NoTracking et que vous essayez de charger le même objet plusieurs fois ? La base de données sera interrogée à chaque fois, ce qui a un impact. Combien de fois devez-vous demander le même objet au cours d'une même requête de page ? Le moins possible bien sûr, mais cela arrive.

Vous vous souvenez également de l'article ci-dessus concernant la connexion automatique des associations pour vous ? Ce n'est pas le cas 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 à l'esprit lorsque vous essayez d'optimiser les performances.

Pas de chargement paresseux, que dois-je faire ?

Cela peut être considéré comme une bénédiction déguisée. Bien sûr, il est ennuyeux de tout charger manuellement. Cependant, cela diminue le nombre d'appels à la base de données et vous oblige à réfléchir au moment où vous devez charger les données. Plus vous pouvez charger de données en un seul appel à la base de données, mieux c'est. Cela a toujours été vrai, mais c'est maintenant renforcé par cette "fonctionnalité" d'EF.

Bien sûr, vous pouvez appeler if( !ObjectReference.IsLoaded ) ObjectReference.Load() ; si vous le souhaitez, mais il est préférable de forcer le framework à charger les objets dont vous savez que vous aurez besoin en une seule fois. C'est là que la discussion sur les inclusions paramétrées prend tout son sens.

Disons que vous avez votre objet Chien

public class Dog
{
    public Dog Get(int id)
    {
        return YourContext.DogSet.FirstOrDefault(it => it.ID == id );
    }
}

C'est le type de fonction avec lequel vous travaillez tout le temps. Elle est appelée de partout et une fois que vous avez cet objet Chien, vous allez lui faire des choses très différentes dans différentes fonctions. Premièrement, il doit être précompilé, 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 chien. Certaines voudront le propriétaire, d'autres le jouet favori, etc.

Bien sûr, vous pouvez 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 lors de la première demande de l'objet Dog :

    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 " +

29voto

Gulzar Nazim Points 35342

[Veuillez insérer la réponse ici] :)

3voto

Adam Tuliper - MSFT Points 22478

Veuillez ne pas utiliser toutes les informations ci-dessus telles que "Singleton access". Vous ne devez absolument pas stocker ce contexte pour le réutiliser car il n'est pas thread safe.

1voto

user48545 Points 456

Bien qu'informatif, je pense qu'il serait plus utile de partager comment tout cela s'intègre dans une architecture de solution complète. Exemple - J'ai une solution où vous utilisez à la fois l'héritage EF et votre alternative pour montrer leur différence de performance.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X