2009-02-13 11 views
74

Questo si applica principalmente per un'applicazione asp.net in cui i dati non sono accessibili tramite soa. Significa che si ottiene l'accesso agli oggetti caricati dal framework, non agli oggetti di trasferimento, anche se alcuni consigli si applicano ancora.Quali sono le buone pratiche di progettazione quando si lavora con Entity Framework

Questo è un post della comunità, quindi per favore aggiungilo come meglio credi.

Si applica a: Entity Framework 1.0 fornito con Visual Studio 2008 sp1.

Perché scegliere EF in primo luogo?

Considerando che si tratta di una tecnologia giovane con molti problemi (vedi sotto), potrebbe essere difficile vendere il carrozzone EF per il tuo progetto. Tuttavia, è la tecnologia che Microsoft sta spingendo (a spese di Linq2Sql, che è un sottoinsieme di EF). Inoltre, potresti non essere soddisfatto con NHibernate o altre soluzioni disponibili. Qualunque sia la ragione, ci sono persone là fuori (me compreso) che lavorano con EF e la vita non è male. Pensaci.

EF ed eredità

Il primo grande soggetto è eredità. EF supporta la mappatura delle classi ereditate che vengono mantenute in 2 modi: tabella per classe e tabella della gerarchia. La modellazione è semplice e non ci sono problemi di programmazione con quella parte.

(Quanto segue si applica alla tabella per modello di classe in quanto non ho esperienza con la tabella per gerarchia, che è, comunque, limitata.) Il vero problema si verifica quando si tenta di eseguire query che includono uno o più oggetti che fanno parte di un albero di ereditarietà: lo sql generato è incredibilmente terribile, richiede molto tempo per essere analizzato dall'EF e richiede molto tempo per essere eseguito. Questo è un vero spettacolo. È sufficiente che EF non venga probabilmente utilizzato con ereditarietà o il meno possibile.

Ecco un esempio di quanto fosse pessimo. Il mio modello EF aveva ~ 30 classi, ~ 10 delle quali facevano parte di un albero ereditario. Eseguendo una query per ottenere un oggetto dalla classe Base, qualcosa di semplice come Base.Get (id), l'SQL generato era di oltre 50.000 caratteri. Quindi, quando si tenta di restituire alcune associazioni, questo degenera ancora di più, arrivando addirittura a generare eccezioni SQL per non essere in grado di eseguire query su più di 256 tabelle contemporaneamente.

Ok, questo non va bene, il concetto EF è quello di consentire di creare la struttura dell'oggetto senza (o con il minimo possibile) considerazione sull'attuale implementazione del database della tabella. Fallisce completamente in questo.

Quindi, raccomandazioni? Evita l'ereditarietà, se puoi, le prestazioni saranno molto migliori. Usalo con parsimonia dove devi. A mio avviso, questo rende EF un glorificato strumento di generazione di sql per l'interrogazione, ma ci sono ancora dei vantaggi nell'utilizzarlo. E i modi per implementare un meccanismo simile all'ereditarietà.

Bypassare eredità con interfacce

prima cosa da sapere con il tentativo di ottenere una sorta di eredità andare con EF è che non si può assegnare una classe di non-EF-modellato una classe di base. Non provarlo nemmeno, verrà sovrascritto dal modellatore. Quindi che si fa?

È possibile utilizzare le interfacce per far sì che le classi implementino alcune funzionalità.Ad esempio, ecco un'interfaccia IEntity che consente di definire associazioni tra entità EF in cui non si conosce in fase di progettazione quale sarebbe il tipo di 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; } } 
} 

Usando questo IEntity, si può quindi lavorare con le associazioni non definiti in altre classi

// 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); 
    } 
} 

che fa uso di alcune funzioni di estensione:

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"); 
     } 
    } 
} 

Non così pulito come avere l'eredità pianura , in particolare considerando che è necessario memorizzare PetType in un campo di database aggiuntivo, ma considerando i guadagni in termini di prestazioni, non guarderei indietro.

Inoltre, non può modellare uno-a-molti, molti-a-molti, ma con usi creativi di 'Unione' potrebbe essere fatto funzionare. Infine, crea l'effetto collaterale del caricamento dei dati in una proprietà/funzione dell'oggetto, di cui occorre fare attenzione. L'uso di una convenzione di denominazione chiara come GetXYZ() aiuta in questo senso.

Compiled Queries

prestazioni Entity Framework non è buono come accesso diretto al database con ADO (ovviamente) o Linq2SQL. Ci sono modi per migliorarlo, tuttavia, uno dei quali è la compilazione delle query. Le prestazioni di una query compilata sono simili a Linq2Sql.

Che cosa è una query compilata? È semplicemente una query per la quale si dice al framework di mantenere in memoria l'albero analizzato in modo che non debba essere rigenerato la prossima volta che lo si esegue. Quindi, alla prossima esecuzione, risparmierai il tempo necessario per analizzare l'albero. Non sottovalutarlo in quanto si tratta di un'operazione molto costosa che diventa ancora peggiore con query più complesse.

Ci sono 2 modi per compilare una query: creare un ObjectQuery con EntitySQL e utilizzando la funzione CompiledQuery.Compile(). (Si noti che utilizzando un EntityDataSource nella tua pagina, verrà infatti utilizzando ObjectQuery con EntitySQL, in modo che venga compilato e cache).

Una parentesi qui nel caso in cui non sai cosa è EntitySQL. È un modo basato su stringhe per scrivere query sull'EF. Ecco un esempio: "seleziona valore cane da Entities.DogSet come cane dove dog.ID = @ID". La sintassi è molto simile alla sintassi SQL. Puoi anche fare una manipolazione di oggetti piuttosto complessa, che è ben spiegata [qui] [1].

Ok, ecco come fare usando 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 prima volta che si esegue questa query, il framework genererà l'albero di espressione e tenerlo in memoria. Quindi la prossima volta che viene eseguito, risparmierai su quel costoso passo. Nell'esempio EnablePlanCaching = true, che non è necessario poiché questa è l'opzione predefinita.

L'altro modo per compilare una query per un utilizzo successivo è il metodo CompiledQuery.Compile.Questo utilizza un delegato:

static readonly Func<Entities, int, Dog> query_GetDog = 
     CompiledQuery.Compile<Entities, int, Dog>((ctx, id) => 
      ctx.DogSet.FirstOrDefault(it => it.ID == id)); 

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

a chiamare la query:

query_GetDog.Invoke(YourContext, id); 

Il vantaggio di CompiledQuery è che la sintassi della query viene verificata al momento della compilazione , dove come EntitySQL non lo è. Tuttavia, ci sono altre considerazioni ...

Include

Diciamo che si desidera avere i dati per il proprietario del cane per essere restituiti dalla query per evitare di fare 2 chiamate al database. Facile da fare, giusto?

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

Ora, che cosa se si vuole avere la Includi parametrizzato? Quello che voglio dire è che vuoi avere una singola funzione Get() che viene chiamata da pagine diverse che si interessano di diverse relazioni per il cane. Uno si interessa al proprietario, un altro al suo FavoriteFood, un altro al suo FavotireToy e così via. In linea di principio, vuoi dire alla query quali associazioni caricare.

E 'facile da fare con 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'includono semplicemente utilizza la stringa passata. Abbastanza facile Si noti che è possibile migliorare la funzione Include (stringa) (che accetta solo un singolo percorso) con IncludeMany (stringa) che consente di passare una stringa di associazioni separate da virgole da caricare. Guarda più avanti nella sezione di estensione per questa funzione.

Se cerchiamo di farlo con CompiledQuery tuttavia, ci imbattiamo in numerosi problemi:

L'ovvio

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

soffocare quando viene chiamato con:

query_GetDog.Invoke(YourContext, id, "Owner,FavoriteFood"); 

Perché, come menzionate sopra, Include() vuole solo vedere un singolo percorso nella stringa e qui lo stiamo dando 2: "Owner" e "FavoriteFood" (che non deve essere confuso con "Owner.FavoriteFood"!).

Quindi, usiamo IncludeMany(), che è una funzione di estensione

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

errato, questa volta è perché l'EF non può analizzare IncludeMany perché non è parte delle funzioni che è riconosciuto: si è un'estensione.

Ok, quindi si desidera passare un numero arbitrario di percorsi alla propria funzione e Include() ne richiede solo uno. Cosa fare? Potresti decidere di non averne mai più bisogno di, diciamo 20 Include, e passare ogni stringa separata in una struttura a CompiledQuery.Ma ora la query appare così:

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 

che è orribile pure. Ok, allora, ma aspetta un minuto. Non possiamo restituire una ObjectQuery <> con CompiledQuery? Quindi imposta gli include su quello? Ebbene, che cosa avrei pensato in modo così:

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; 
} 

che dovrebbero avere lavorato, tranne che quando si chiama IncludeMany (o includere, Dove, OrderBy ...) si invalidare la query compilata nella cache perché è uno completamente nuovo ora! Quindi, l'albero delle espressioni deve essere ripetuto e si ottiene di nuovo il successo della performance.

Quindi qual è la soluzione? Semplicemente non è possibile utilizzare CompiledQueries con Include parametrizzati. Utilizzare invece EntitySQL. Questo non significa che non ci siano usi per CompiledQueries. È ottimo per le query localizzate che verranno sempre chiamate nello stesso contesto. Idealmente CompiledQuery dovrebbe sempre essere usato perché la sintassi è verificata in fase di compilazione, ma a causa della limitazione, ciò non è possibile.

Un esempio di utilizzo potrebbe essere: si consiglia di avere una pagina che le query che due cani hanno lo stesso cibo preferito, che è un po 'stretta per una funzione BusinessLayer, così lo metti nella tua pagina e sanno esattamente cosa tipo di include sono obbligatori.

Passando più di 3 parametri a una CompiledQuery

Func è limitato a 5 parametri, di cui l'ultimo è il tipo di ritorno e il primo è le entità oggetto dal modello. Quindi questo ti lascia con 3 parametri. Una carenza, ma può essere migliorata molto facilmente.

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

Tipi restituiti (questo non si applica a EntitySQL query in quanto non vengono compilati nello stesso momento durante l'esecuzione del metodo CompiledQuery)

Lavorando con LINQ, di solito non si forzi l'esecuzione della query, fino all'ultimo momento, nel caso in cui alcune altre funzioni vuole valle per modificare la query in qualche modo:

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

} 

che cosa sta per succedere qui? Continuando a giocare con l'ObjectQuery originale (che è il tipo di ritorno effettivo dell'istruzione Linq, che implementa IEnumerable), invaliderà la query compilata e sarà forzata a ri-analizzare. Pertanto, la regola generale consiste nel restituire un elenco <> di oggetti.

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

} 

quando si chiama ToList(), la query viene eseguito come per la query compilata e poi, più tardi, l'OrderBy viene eseguita contro gli oggetti in memoria. Potrebbe essere un po 'più lento, ma non ne sono nemmeno sicuro. Una cosa certa è che non ci si deve preoccupare di gestire male ObjectQuery e invalidare il piano di query compilato.

Ancora una volta, questa non è una dichiarazione generale. ToList() è un trucco di programmazione difensivo, ma se hai una ragione valida per non usare ToList(), vai avanti. Ci sono molti casi in cui vorresti perfezionare la query prima di eseguirla.

prestazioni

Qual è l'impatto sulle prestazioni di compilazione di una query? In realtà può essere abbastanza grande.Una regola empirica è che la compilazione e la memorizzazione nella cache della query per il riutilizzo richiede almeno il doppio del tempo di esecuzione semplicemente senza memorizzazione nella cache. Per query complesse (leggi inherirante), ho visto fino a 10 secondi.

Quindi, la prima volta che viene chiamata una query precompilata, si ottiene un risultato in termini di prestazioni. Dopo questo primo hit, le prestazioni sono notevolmente migliori della stessa query non precompilata. Praticamente uguale a Linq2Sql

Quando si carica una pagina con query precompilate la prima volta si ottiene un hit. Caricherà in forse 5-15 secondi (ovviamente più di una query pre-compilata finirà per essere chiamata), mentre i carichi successivi richiederanno meno di 300ms. Drammatica differenza, e spetta a te decidere se è ok per il tuo primo utente ottenere un successo o se vuoi che uno script chiami le tue pagine per forzare una compilazione delle query.

Questa query può essere memorizzata nella cache?

{ 
    Dog dog = from dog in YourContext.DogSet where dog.ID == id select dog; 
} 

No, le query LINQ ad-hoc non vengono memorizzati nella cache e si dovranno sostenere il costo di generare l'albero ogni volta si chiama.

parametrizzate Query

La maggior parte delle funzionalità di ricerca coinvolgono pesantemente query parametrizzate. Sono disponibili anche librerie che ti consentiranno di creare una query parametrizzata con espressioni lamba. Il problema è che non è possibile utilizzare query precompilate con quelle. Un modo per aggirare questo è quello di tracciare tutte le possibili criteri nella query e la bandiera che uno si desidera utilizzare:

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

Il vantaggio è che si ottengono tutti i benifits di un Quert pre-compilato. Gli svantaggi sono che molto probabilmente si finisce con una clausola where che è piuttosto difficile da mantenere, che si incorre in una penalità maggiore per la pre-compilazione della query e che ogni query eseguita non è efficiente come potrebbe essere (in particolare con i join gettati dentro).

Un altro modo è creare una query EntitySQL pezzo per pezzo, come tutti abbiamo fatto con 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(); 
} 

Qui i problemi sono: - non v'è alcun controllo della sintassi durante la compilazione - ognuna diversa combinazione di parametri di generare una query diversa, che dovranno essere pre-compilato quando è prima esecuzione. In questo caso, ci sono solo 4 diverse query possibili (nessun parametro, età solo, solo nome ed entrambi i parametri), ma puoi vedere che ci può essere molto di più con una normale ricerca mondiale. - A nessuno piace concatenare le stringhe!

Un'altra opzione è interrogare un grande sottoinsieme di dati e quindi restringerlo in memoria. Ciò è particolarmente utile se si lavora con un sottoinsieme definito di dati, come tutti i cani in una città. Sai che ce ne sono molti ma sai anche che non ce ne sono così tanti ... quindi la tua pagina di ricerca di CityDog può caricare in memoria tutti i cani per la città, che è una singola query pre-compilata e quindi raffinare i risultati

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; 
} 

È particolarmente utile quando si inizia a visualizzare tutti i dati, quindi si consente il filtraggio.

Problemi: - Potrebbe portare a un serio trasferimento di dati se non si presta attenzione al proprio sottoinsieme. - È possibile filtrare solo i dati restituiti.Significa che se non restituisci l'associazione Dog.Owner, non potrai filtrare su Dog.Owner.Name Quindi qual è la soluzione migliore? Non ce n'è. Devi scegliere la soluzione più adatta a te e al tuo problema: - Utilizza la creazione di query basata su lambda quando non ti interessa la pre-compilazione delle tue query. - Usa query Linq precompilata completamente definita quando la struttura dell'oggetto non è troppo complessa. - Utilizzare EntitySQL/concatenazione di stringhe quando la struttura potrebbe essere complessa e quando il numero possibile di diverse query risultanti è di piccole dimensioni (il che significa un minor numero di hit pre-compilazione). - Usa il filtraggio in memoria quando lavori con un sottoinsieme di dati piuttosto piccolo o quando devi prima recuperare tutti i dati sui dati (se le prestazioni vanno bene con tutti i dati, allora il filtraggio in memoria non causare alcun tempo da spendere nel db).

Singleton accesso

Il modo migliore per trattare con il vostro contesto e le entità attraversato tutte le pagine è quello di utilizzare il pattern 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, ne vale la pena?

Quando si esegue una query, è possibile indicare al framework di tracciare gli oggetti che restituirà o meno. Cosa significa? Con il tracciamento abilitato (opzione predefinita), il framework tiene traccia di ciò che sta accadendo con l'oggetto (è stato modificato? Creato? Eliminato?) E collegherà anche gli oggetti insieme, quando ulteriori query vengono fatte dal database, che è ciò che è di interesse qui.

Per esempio, lascia supporre che Cane con ID == 2 ha un proprietario che 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; 

Se dovessimo fare lo stesso con nessun inseguimento, il risultato sarebbe stato diverso.

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; 

Il tracciamento è molto utile e in un mondo perfetto senza problemi di prestazioni, sarebbe sempre attivo. Ma in questo mondo, c'è un prezzo per esso, in termini di prestazioni. Quindi, dovresti usare NoTracking per velocizzare le cose? Dipende da ciò per cui prevedi di utilizzare i dati.

Esiste la possibilità che i dati della query con NoTracking possano essere utilizzati per effettuare l'aggiornamento/inserimento/eliminazione nel database? In tal caso, non utilizzare NoTracking perché le associazioni non vengono tracciate e causeranno il lancio di eccezioni.

In una pagina in cui non ci sono assolutamente aggiornamenti al database, è possibile utilizzare NoTracking.

Il tracciamento della miscelazione e NoTracking è possibile, ma richiede una maggiore attenzione con gli aggiornamenti/inserimenti/eliminazioni. Il problema è che se si mischia, si rischia che il framework provi a collegare() un oggetto NoTracking al contesto in cui esiste un'altra copia dello stesso oggetto con il tracciamento. Fondamentalmente, quello che sto dicendo è che

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 e dog2 sono 2 oggetti diversi, uno tracciato e uno non. L'utilizzo dell'oggetto distaccato in un aggiornamento/inserimento costringerà un Attach() che dirà "Aspetta un minuto, ho già un oggetto qui con la stessa chiave di database. E quando Alleghi() un oggetto, anche tutta la sua gerarchia viene attaccata, causando problemi ovunque. Stai molto attento.

Quanto è più veloce con NoTracking

Dipende dalle query. Alcuni sono molto più suscettibili al tracciamento rispetto ad altri. Non ho una regola facile e veloce, ma aiuta.

Quindi dovrei usare NoTracking ovunque allora?

Non esattamente. Ci sono alcuni vantaggi nell'oggetto di tracciamento. Il primo è che l'oggetto è memorizzato nella cache, quindi la successiva chiamata per quell'oggetto non colpirà il database. Quella cache è valida solo per la durata dell'oggetto YourEntities, che, se si utilizza il codice singleton sopra, è uguale alla durata della pagina. Richiesta di una sola pagina == un oggetto YourEntity. Pertanto, per più chiamate per lo stesso oggetto, verrà caricata solo una volta per richiesta di pagina. (Un altro meccanismo di memorizzazione nella cache potrebbe estenderlo).

Cosa succede quando si utilizza NoTracking e si tenta di caricare lo stesso oggetto più volte? Il database verrà interrogato ogni volta, quindi c'è un impatto lì. Quanto spesso dovresti/dovresti chiamare lo stesso oggetto durante una singola richiesta di pagina? Il meno possibile, naturalmente, ma succede.

Ricorda anche il pezzo precedente sull'avere collegato automaticamente le associazioni per il tuo? Non è necessario che, con NoTracking, quindi se si caricano i dati in più lotti, non avrete un collegamento a tra di loro:

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

In questo caso, nessun cane avrà il suo set di proprietà .Owner.

Alcune cose da tenere a mente quando si tenta di ottimizzare le prestazioni.

Nessun caricamento lazy, cosa devo fare?

Questo può essere visto come una benedizione sotto mentite spoglie. Ovviamente è fastidioso caricare tutto manualmente. Tuttavia, diminuisce il numero di chiamate al db e ti costringe a pensare quando caricare i dati. Quanto più è possibile caricare in una chiamata al database, tanto meglio. Era sempre vero, ma ora è applicato con questa "funzionalità" di EF.

Ovviamente, è possibile chiamare if (! ObjectReference.IsLoaded) ObjectReference.Load(); se lo desideri, ma una pratica migliore è forzare il framework a caricare gli oggetti di cui avrai bisogno in un colpo solo. Questo è il punto in cui la discussione sui Comprimi parametrizzati comincia a dare un senso.

Diciamo che avete Dog oggetto

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

Questo è il tipo di funzione si lavora con tutto il tempo. Viene chiamato da tutte le parti e una volta ottenuto l'oggetto Dog, si faranno cose molto diverse con funzioni diverse. In primo luogo, dovrebbe essere precompilato, perché lo chiamerai molto spesso. In secondo luogo, ogni pagina diversa vorrà avere accesso a un diverso sottoinsieme dei dati Dog. Alcuni vorranno il proprietario, alcuni il FavoriteToy, ecc.

Ovviamente, puoi chiamare Load() per ogni riferimento che ti serve ogni volta che ne hai bisogno. Ma questo genererà una chiamata al database ogni volta. Cattiva idea.Così, invece, ogni pagina vi chiederà per i dati che vuole vedere, quando la prima richiesta per l'oggetto Cane:

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

Seriamente, non hai un blog o un sito web tutto tuo dove potresti aver postato questo articolo? – AnthonyWJones

+7

@AnthonyWJones, beh, anche se non è il solito post su SO; se ascolti il ​​SO Podcast, Jeff usa sempre il mantra che SO è un luogo in cui i programmatori possono condividere conoscenze che non hanno blog. Questo NON è offensivo. E basato sul mantra di Jeff, non dovrebbe essere chiuso a mio avviso. – BobbyShaftoe

+0

Giusto per aggiungere, penso che sia anche interessante che questo post verrebbe probabilmente upvotato più volte se fosse una risposta alla domanda; la domanda potrebbe essere solo il titolo; eppure la gente lo segna come offensivo ... strano. – BobbyShaftoe

risposta

1

Mentre informativo Penso che possa essere più utile per condividere come tutto questo si inserisce in un'architettura soluzione completa . Esempio: una soluzione che mostra dove si utilizza l'ereditarietà EF e l'alternativa in modo che mostri la differenza di prestazioni.

3

Si prega di non utilizzare tutte le informazioni di cui sopra come "accesso Singleton". Assolutamente 100% non deve memorizzare questo contesto per essere riutilizzato in quanto non è thread-safe.

+0

+1 è certamente contro l'approccio dell'unità di lavoro. Non supporta l'annullamento delle modifiche e, quando si salva il contesto, si avrà difficoltà a sapere cosa è stato effettivamente modificato dall'ultimo salvataggio. – surfen

Problemi correlati