Quelles sont les bonnes pratiques de conception lorsque vous utilisez Entity Framework?

asp.net entity-framework visual-studio-2008

Question

Cela s'appliquera 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 à partir de la structure, et non à Transférer des objets, bien que certaines recommandations s'appliquent toujours.

Ceci est un message communautaire, donc ajoutez-le comme bon vous semble.

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

Pourquoi choisir EF en premier lieu?

Considérant qu’il s’agit d’une technologie jeune qui pose de nombreux problèmes (voir ci-dessous), il peut être difficile de se lancer dans le mouvement EF pour votre projet. C’est toutefois la technologie que Microsoft préconise (aux dépens de Linq2Sql, qui est un sous-ensemble de EF). De plus, il est possible que vous ne soyez pas satisfait de NHibernate ou d’autres solutions. Quelles que soient les raisons, il y a des gens (y compris moi) qui travaillent avec EF et la vie n’est pas mauvaise.

EF et héritage

Le premier grand sujet est l'héritage. EF prend en charge le mappage pour les classes héritées persistantes de deux manières: table par classe et table de la hiérarchie. La modélisation est facile et cette partie ne pose aucun problème de programmation.

(Ce qui suit s'applique au modèle table par classe car je n'ai aucune expérience de la table par hiérarchie, ce qui est limité de toute façon.) Le vrai problème survient lorsque vous essayez d'exécuter des requêtes qui incluent un ou plusieurs objets faisant partie de un arbre d'héritage: le sql généré est incroyablement horrible, il faut beaucoup de temps pour qu'il soit analysé par EF et il faut aussi beaucoup de temps pour qu'il s'exécute. C'est un vrai spectacle. Assez pour que EF ne soit probablement pas utilisé avec un héritage ou le moins possible.

Voici un exemple de la gravité de la situation. Mon modèle EF comportait environ 30 classes, dont environ 10 faisaient partie d'un arbre d'héritage. Lors de l'exécution d'une requête pour obtenir un élément de la classe de base, quelque chose d'aussi simple que Base.Get (id), le code SQL généré comptait plus de 50 000 caractères. Ensuite, lorsque vous essayez de renvoyer certaines associations, cela dégénère encore plus, allant même jusqu'à renvoyer des exceptions SQL sur l'impossibilité d'interroger plus de 256 tables à la fois.

Ok, c’est mauvais, le concept EF est de vous permettre de créer votre structure d’objet sans (ou avec le moins possible) de considération pour l’implémentation réelle de votre table dans la base de données. Il échoue complètement à cela.

Alors, des recommandations? Évitez l'héritage si vous le pouvez, la performance sera bien meilleure. Utilisez-le avec parcimonie là où vous devez. À mon avis, cela fait de EF un outil privilégié de génération de requêtes SQL, mais son utilisation présente néanmoins des avantages. Et des moyens de mettre en œuvre un mécanisme similaire à l'héritage.

Contournement de l'héritage avec des interfaces

La première chose à savoir pour essayer d'obtenir un héritage avec EF est que vous ne pouvez pas affecter une classe de base à une classe non modélisée par EF. N'essayez même pas, il sera écrasé par le modélisateur. Alors que faire?

Vous pouvez utiliser des interfaces pour faire en sorte 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 savez pas au moment du design le type de cette 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 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 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 bien que d’avoir un héritage simple, surtout si vous devez stocker le PetType dans un champ de base de données supplémentaire, mais compte tenu des gains de performances, je ne reviendrais pas en arrière.

Il ne peut pas non plus modéliser une relation un à plusieurs, mais il pourrait être exploité avec des utilisations créatives de "Union". Enfin, il crée l'effet secondaire du chargement de données dans une propriété / fonction de l'objet, sur lequel vous devez faire attention. L'utilisation d'une convention de dénomination claire telle que GetXYZ () aide à cet égard.

Requêtes compilées

Les performances d'Entity Framework ne sont pas aussi performantes que l'accès direct à la base de données avec ADO (évidemment) ou Linq2SQL. Il existe toutefois des moyens de l'améliorer, notamment 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 en mémoire l'arborescence analysée afin qu'il ne soit pas nécessaire de la régénérer à la prochaine exécution. Ainsi, lors de la prochaine exécution, vous épargnerez le temps nécessaire pour analyser l’arbre. Ne négligez pas cela car il s’agit d’une opération très coûteuse qui s’aggrave encore avec des requêtes plus complexes.

Il existe deux 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 utiliserez ObjectQuery avec EntitySQL, ce qui le compilera et le mettra en cache).

Un côté ici si vous ne savez pas ce qu'est EntitySQL. C'est un moyen basé sur des chaînes pour écrire des requêtes sur l'EF. Voici un exemple: "Sélectionnez chien de valeur dans Entities.DogSet en tant que chien où dog.ID = @ID". La syntaxe est assez similaire à la syntaxe SQL. Vous pouvez également faire des manipulations d'objets assez complexes, ce qui est bien expliqué [ici] [1].

Ok, alors voici comment faire avec 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érera l'arborescence des expressions et le conservera en mémoire. Ainsi, lors de la prochaine exécution, vous économiserez sur cette étape coûteuse. Dans cet exemple, EnablePlanCaching = true, ce qui est inutile car il s'agit de l'option par défaut.

L'autre moyen de compiler une requête pour une utilisation ultérieure est la méthode CompiledQuery.Compile. Ceci 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());

appeler la requête:

query_GetDog.Invoke( YourContext, id );

CompiledQuery présente l'avantage de vérifier la syntaxe de votre requête lors de la compilation, contrairement à EntitySQL. Cependant, il y a une autre considération ...

Comprend

Disons que vous souhaitez que les données du propriétaire du chien soient renvoyées par la requête afin d'éviter de passer 2 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 souhaitez paramétrer l’inclusion? Ce que je veux dire, c'est que vous voulez avoir une seule fonction Get () appelée à partir de pages différentes qui se préoccupent de relations différentes pour le chien. L'un se soucie du propriétaire, un autre de son FavoriteFood, un autre de son FavotireToy, 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'inclusion utilise simplement la chaîne passée. Assez facile. Notez qu'il est possible d'améliorer la fonction Include (chaîne) (qui n'accepte qu'un seul chemin) avec un IncludeMany (chaîne) qui vous permettra de transmettre 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'évident

    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 s'étouffer quand 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 nous lui donnons ici 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 une fois, c'est parce que l'EF ne peut pas analyser IncludeMany car il ne fait pas partie des fonctions reconnues: il s'agit d'une extension.

Ok, vous voulez donc transmettre un nombre arbitraire de chemins à votre fonction et Includes () n'en prend qu'un seul. Que faire? Vous pouvez décider de ne jamais avoir besoin de plus de, par exemple, 20 includes, et transmettre 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 terrible aussi. Ok, alors, mais attendez une minute. Ne pouvons-nous pas renvoyer une ObjectQuery <> avec CompiledQuery? Puis définissez l'inclus sur cela? Eh bien, c'est ce que j'aurais pensé aussi bien:

    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 d'une toute nouvelle requête maintenant! Il faut donc reparer l'arbre d'expression et obtenir à nouveau cette performance.

Donc, quelle est la solution? Vous ne pouvez simplement pas utiliser CompiledQueries avec une inclusion paramétrée. Utilisez EntitySQL à la place. Cela ne signifie pas qu'il n'y a pas d'utilisations de CompiledQueries. C'est très bien 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 de limitations, cela n'est pas possible.

Voici un exemple d'utilisation: vous voudrez peut-être une page qui demande à deux chiens qui ont le même aliment préféré, ce qui est un peu étroit pour une fonction BusinessLayer, vous le mettez donc dans votre page et savez exactement quel type d'inclusions sont Champs obligatoires.

Passer plus de 3 paramètres à une requête 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. Cela vous laisse donc 3 paramètres. Une pitance, 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 (cela 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ù d'autres fonctions en aval souhaiteraient 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 );

}

Qu'est-ce qui va se passer ici? En continuant à jouer avec ObjectQuery d'origine (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 la ré-analyse forcée. Ainsi, la règle de base est de renvoyer une liste <> d'objets.

    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, OrderBy est exécuté sur les objets en mémoire. C'est peut-être un peu plus lent, mais je n'en suis même pas sûr. Une chose est sûre: vous ne vous inquiétez pas de la mauvaise gestion d'ObjectQuery et de l'invalidation du plan de requête compilé.

Encore une fois, ce n'est pas 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 (), continuez. Il existe de nombreux cas dans lesquels 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? En réalité, il peut être assez volumineux. 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 le double du temps nécessaire pour l'exécuter sans la mettre en cache. Pour les requêtes complexes (lire inherirante), j'ai vu jusqu'à 10 secondes.

Ainsi, la première fois qu'une requête pré-compilée est appelée, vous obtenez un impact négatif sur les performances. Après ce premier coup, les performances sont sensiblement meilleures que la même requête non pré-compilée. Pratiquement identique à Linq2Sql

Lorsque vous chargez une page avec des requêtes pré-compilées pour la première fois, vous obtenez un hit. Il se chargera peut-être dans 5-15 secondes (évidemment, 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 dramatique, et il vous appartient de décider si votre premier utilisateur accepte un hit ou si vous souhaitez qu'un script 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 devrez supporter le coût de la génération de l'arborescence chaque fois que vous l'appelez.

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 disponibles qui vous permettront 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 celles-ci. Une solution consiste à définir tous les critères possibles dans la requête et à indiquer celui que 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 avantages d'une requête pré-compilée. L’inconvénient est que vous allez très probablement vous retrouver avec une clause where assez difficile à maintenir, que vous encourrez une pénalité plus lourde pour la pré-compilation de la requête et que chaque requête que vous exécutez n’est pas aussi efficace qu’elle pourrait être (en particulier avec joint jeté dedans).

Une autre méthode consiste à créer une requête EntitySQL pièce par pièce, 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 vérification de la syntaxe lors de la compilation - chaque combinaison 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 (pas de paramètre, âge seulement, nom seul et les deux paramètres), mais vous pouvez voir qu'il peut y avoir beaucoup plus de choses avec une recherche mondiale normale. - Personne n'aime enchaîner les ficelles!

Une autre option consiste à interroger un grand sous-ensemble de données, puis à le réduire en mémoire. Ceci 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 beaucoup ... donc votre page de recherche CityDog peut charger tous les chiens de la ville en mémoire, ce qui correspond à 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;
}

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

Problèmes: - Cela pourrait entraîner de graves transferts de données si vous ne faites pas attention à votre sous-ensemble. - Vous ne pouvez filtrer que sur les données que vous avez renvoyé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 vous convient le mieux, à vous et à votre problème: - Utilisez le développement de requêtes lambda lorsque vous ne vous souciez pas de la pré-compilation de vos requêtes. - Utilisez une requête Linq précompilée entièrement définie lorsque la structure de votre objet n'est pas trop complexe. - Utilisez la concaténation EntitySQL / string lorsque la structure peut être complexe et que le nombre possible de requêtes différentes résultantes est faible (ce qui signifie moins de hits de pré-compilation). - Utilisez le filtrage en mémoire lorsque vous travaillez avec un petit sous-ensemble de données ou lorsque vous devez extraire d’abord toutes les données des données (si les performances sont satisfaisantes avec toutes les données, le filtrage en mémoire ne faire passer le temps dans la base de données).

Accès singleton

Le meilleur moyen 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, ça vaut le coup?

Lorsque vous exécutez une requête, vous pouvez indiquer au framework de suivre les objets qu’elle renverra ou non. Qu'est-ce que ça veut dire? Lorsque 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 liera également les objets ensemble, lorsque d'autres requêtes seront effectuées à partir de la base de données. est d'intérêt ici.

Par exemple, supposons que Dog avec ID == 2 ait un propriétaire dont 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 devions faire 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 performances, il serait toujours activé. Mais dans ce monde, il y a un prix à payer pour cela, en termes de performance. Alors, devriez-vous utiliser NoTracking pour accélérer les choses? Cela dépend de ce que vous prévoyez d’utiliser les données.

Existe-t-il une chance que les données de votre requête avec NoTracking puissent être utilisées pour mettre à jour / insérer / supprimer dans la base de données? Si c'est le cas, n'utilisez pas NoTracking car les associations ne sont pas suivies et provoqueront 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 cela nécessite une grande prudence lors de la mise à jour / insertion / suppression. Le problème est que si vous mélangez, vous risquez de voir le framework essayer d'attacher () un objet NoTracking au contexte dans lequel une autre copie du même objet existe avec le suivi. Fondamentalement, ce que je dis 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 deux objets différents, l'un suivi et l'autre non. L'utilisation de l'objet détaché dans une mise à jour / une insertion forcera un Attach () à dire "Attendez une minute, j'ai déjà un objet ici avec la même clé de base de données. Fail". Et lorsque vous attachez un objet, toute sa hiérarchie est également attachée, ce qui pose des problèmes partout. Soyez très prudent.

Combien de temps est-il plus rapide avec NoTracking

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

Donc je devrais utiliser NoTracking partout alors?

Pas exactement. Il existe certains avantages à suivre un objet. La première est que l'objet est mis en cache, ainsi l'appel suivant pour cet objet ne sera pas touché 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 identique à la durée de vie de la page. Une demande de page == un objet YourEntity. Donc, pour plusieurs appels pour le même objet, il ne sera chargé qu'une seule fois par demande de page. (Un autre mécanisme de cache pourrait prolonger 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 aura donc un impact. À quelle fréquence appelez-vous / devriez-vous utiliser le même objet lors d'une demande de page unique? Le moins possible, bien sûr, mais cela arrive.

Rappelez-vous également la pièce ci-dessus sur la connexion automatique des associations pour votre? 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.

Certaines choses à 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 embêtant de tout charger manuellement. Cependant, cela réduit le nombre d'appels à la base de données et vous oblige à penser au moment où vous devez charger des données. Plus vous pouvez charger dans un appel de base de données, mieux c'est. C'était toujours vrai, mais il est maintenant appliqué avec cette "fonctionnalité" de EF.

Bien sûr, vous pouvez appeler if (! ObjectReference.IsLoaded) ObjectReference.Load (); si vous le souhaitez, mais une meilleure pratique consiste à forcer le cadre à charger les objets dont vous savez qu'il vous faudra d'un coup. C’est là que la discussion sur les inclus paramétrés commence à prendre un 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. Il est appelé de partout et une fois que vous avez cet objet Dog, vous allez faire des choses très différentes pour lui dans différentes fonctions. Premièrement, il devrait être pré-compilé, car vous appelez cela très souvent. Deuxièmement, chaque page différente voudra avoir accès à un sous-ensemble différent des données de chien. Certains voudront le propriétaire, d'autres le FavoriteToy, etc.

Bien sûr, vous pouvez appeler Load () pour chaque référence dont vous avez besoin à tout moment. Mais cela générera un appel à la base de données à chaque fois. Mauvaise idée. Au lieu de cela, chaque page demandera les données qu’elle veut voir lorsqu’elle demande pour 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 " +

Réponse populaire

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



Related

Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow