Entity Framework
Tecniche di ottimizzazione in EF
Ricerca…
Utilizzando AsNoTracking
Cattivo esempio:
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Poiché il codice sopra riportato restituisce semplicemente un'entità senza modificarla o aggiungerla, possiamo evitare il costo di tracciamento.
Buon esempio:
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Quando usiamo la funzione AsNoTracking()
stiamo esplicitamente dicendo a Entity Framework che le entità non sono tracciate dal contesto. Questo può essere particolarmente utile quando si recuperano grandi quantità di dati dall'archivio dati. Tuttavia, se si desidera apportare modifiche a entità non tracciabili, è necessario ricordarsi di collegarle prima di chiamare SaveChanges
.
Caricamento solo dati richiesti
Un problema spesso visto nel codice è il caricamento di tutti i dati. Ciò aumenterà notevolmente il carico sul server.
Diciamo che ho un modello chiamato "posizione" che contiene 10 campi, ma non tutti i campi sono richiesti allo stesso tempo. Diciamo che voglio solo il parametro "LocationName" di quel modello.
Cattivo esempio
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location.Name;
Buon esempio
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => l.LocationName);
.SingleOrDefault();
return location;
Il codice nel "buon esempio" recupererà solo "LocationName" e nient'altro.
Si noti che dal momento che nessuna entità è materializzata in questo esempio, AsNoTracking()
non è necessario. Non c'è niente da rintracciare comunque.
Recupero di più campi con tipi anonimi
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => new { Name = l.LocationName, Area = l.LocationArea })
.SingleOrDefault();
return location.Name + " has an area of " + location.Area;
Come nell'esempio precedente, solo i campi "LocationName" e "LocationArea" verranno recuperati dal database, il tipo anonimo può contenere tutti i valori desiderati.
Esegui query nel database quando possibile, non in memoria.
Supponiamo di voler contare quante contee ci sono in Texas:
var counties = dbContext.States.Single(s => s.Code == "tx").Counties.Count();
La query è corretta, ma inefficiente. States.Single(…)
carica uno stato dal database. Successivamente, Counties
carica tutte le 254 contee con tutti i loro campi in una seconda query. .Count()
viene quindi eseguito in memoria nella raccolta Counties
caricata.
Abbiamo caricato molti dati di cui non abbiamo bisogno e possiamo fare di meglio:
var counties = dbContext.Counties.Count(c => c.State.Code == "tx");
Qui facciamo una sola query, che in SQL si traduce in un conteggio e un join. Restituiamo solo il conteggio dal database: abbiamo salvato righe di ritorno, campi e creazione di oggetti.
È facile vedere dove viene eseguita la query osservando il tipo di raccolta: IQueryable<T>
vs. IEnumerable<T>
.
Esegui più query asincrone e in parallelo
Quando si utilizzano query asincrone, è possibile eseguire più query contemporaneamente, ma non nello stesso contesto. Se il tempo di esecuzione di una query è 10 secondi, il tempo per l'esempio errato sarà 20 secondi, mentre il tempo per il buon esempio sarà 10 secondi.
Cattivo esempio
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
using(var context = new Context())
{
result1 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
result2 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
Buon esempio
public async Task<IEnumerable<TResult>> GetResult<TResult>()
{
using(var context = new Context())
{
return await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
}
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
var result1Task = GetResult<TResult1>();
var result2Task = GetResult<TResult2>();
await Task.WhenAll(result1Task, result2Task).ConfigureAwait(false);
var result1 = result1Task.Result;
var result2 = result2Task.Result;
Disabilitare il rilevamento delle modifiche e la generazione del proxy
Se desideri solo ottenere dati, ma non modificare nulla, puoi disattivare il rilevamento delle modifiche e la creazione del proxy. Ciò migliorerà le prestazioni e impedirà anche il caricamento lento.
Cattivo esempio:
using(var context = new Context())
{
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
Buon esempio:
using(var context = new Context())
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
È particolarmente comune disattivarle all'interno del costruttore del contesto, specialmente se si desidera che queste siano impostate sulla propria soluzione:
public class MyContext : DbContext
{
public MyContext()
: base("MyContext")
{
Configuration.AutoDetectChangesEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
//snip
}
Lavorare con entità stub
Supponiamo di avere Product
e Category
in una relazione molti-a-molti:
public class Product
{
public Product()
{
Categories = new HashSet<Category>();
}
public int ProductId { get; set; }
public string ProductName { get; set; }
public virtual ICollection<Category> Categories { get; private set; }
}
public class Category
{
public Category()
{
Products = new HashSet<Product>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Se vogliamo aggiungere una Category
a un Product
, dobbiamo caricare il prodotto e aggiungere la categoria alle sue Categories
, ad esempio:
Cattivo esempio:
var product = db.Products.Find(1);
var category = db.Categories.Find(2);
product.Categories.Add(category);
db.SaveChanges();
(dove db
è una sottoclasse DbContext
).
Questo crea un record nella tabella di giunzione tra Product
e Category
. Tuttavia, questa tabella contiene solo due valori Id
. È uno spreco di risorse caricare due entità complete per creare un record minuscolo.
Un modo più efficiente è utilizzare le entità stub , cioè gli oggetti entità, creati in memoria, contenenti solo il minimo di dati, di solito solo un valore Id
. Questo è quello che sembra:
Buon esempio:
// Create two stub entities
var product = new Product { ProductId = 1 };
var category = new Category { CategoryId = 2 };
// Attach the stub entities to the context
db.Entry(product).State = System.Data.Entity.EntityState.Unchanged;
db.Entry(category).State = System.Data.Entity.EntityState.Unchanged;
product.Categories.Add(category);
db.SaveChanges();
Il risultato finale è lo stesso, ma evita due roundtrip al database.
Prevenire i duplicati
Se vuoi verificare se l'associazione esiste già, è sufficiente una query economica. Per esempio:
var exists = db.Categories.Any(c => c.Id == 1 && c.Products.Any(p => p.Id == 14));
Di nuovo, questo non caricherà le entità complete nella memoria. Effettua query sulla tabella di giunzione e restituisce solo un valore booleano.