Entity Framework
Techniki optymalizacji w EF
Szukaj…
Korzystanie z AsNoTracking
Zły przykład:
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Ponieważ powyższy kod po prostu zwraca jednostkę bez modyfikacji lub dodawania, możemy uniknąć kosztów śledzenia.
Dobry przykład:
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Kiedy używamy funkcji AsNoTracking()
, wyraźnie mówimy Entity Framework, że jednostki nie są śledzone przez kontekst. Może to być szczególnie przydatne podczas pobierania dużych ilości danych z magazynu danych. Jeśli jednak chcesz wprowadzić zmiany w SaveChanges
, musisz pamiętać o dołączeniu ich przed wywołaniem SaveChanges
.
Ładowanie tylko wymaganych danych
Jednym z problemów często spotykanych w kodzie jest ładowanie wszystkich danych. To znacznie zwiększy obciążenie serwera.
Załóżmy, że mam model o nazwie „lokalizacja”, który zawiera 10 pól, ale nie wszystkie pola są wymagane w tym samym czasie. Powiedzmy, że chcę tylko parametr „LocationName” tego modelu.
Zły przykład
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location.Name;
Dobry przykład
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => l.LocationName);
.SingleOrDefault();
return location;
Kod w „dobrym przykładzie” pobierze tylko „LocationName” i nic więcej.
Zauważ, że ponieważ w tym przykładzie nie AsNoTracking()
, AsNoTracking()
nie jest konieczny. Zresztą i tak nie ma nic do śledzenia.
Pobieranie większej liczby pól za pomocą Anonimowych typów
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;
Tak jak w poprzednim przykładzie, tylko pola „LocationName” i „LocationArea” zostaną pobrane z bazy danych, typ anonimowy może przechowywać tyle wartości, ile chcesz.
W miarę możliwości wykonuj zapytania w bazie danych, a nie w pamięci.
Załóżmy, że chcemy policzyć, ile hrabstw jest w Teksasie:
var counties = dbContext.States.Single(s => s.Code == "tx").Counties.Count();
Zapytanie jest poprawne, ale nieefektywne. States.Single(…)
ładuje stan z bazy danych. Następnie Counties
ładują wszystkie 254 hrabstwa ze wszystkimi swoimi polami w drugim zapytaniu. .Count()
jest następnie wykonywane w pamięci załadowanej kolekcji Counties
.
Załadowaliśmy wiele danych, których nie potrzebujemy, i możemy to zrobić lepiej:
var counties = dbContext.Counties.Count(c => c.State.Code == "tx");
Tutaj wykonujemy tylko jedno zapytanie, które w SQL przekłada się na liczbę i sprzężenie. Zwracamy tylko liczbę z bazy danych - zapisaliśmy zwracane wiersze, pola i tworzenie obiektów.
Łatwo jest zobaczyć, gdzie powstaje zapytanie, patrząc na typ kolekcji: IQueryable<T>
vs. IEnumerable<T>
.
Wykonaj wiele zapytań asynchronicznie i równolegle
Korzystając z zapytań asynchronicznych, możesz wykonywać wiele zapytań jednocześnie, ale nie w tym samym kontekście. Jeśli czas wykonania jednego zapytania wynosi 10 s, czas dla złego przykładu wyniesie 20 s, a dla dobrego przykładu 10 s.
Zły przykład
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);
}
Dobry przykład
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;
Wyłącz śledzenie zmian i generowanie proxy
Jeśli chcesz tylko pobierać dane, ale niczego nie modyfikować, możesz wyłączyć śledzenie zmian i tworzenie proxy. Poprawi to twoją wydajność, a także zapobiegnie leniwemu ładowaniu.
Zły przykład:
using(var context = new Context())
{
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
Dobry przykład:
using(var context = new Context())
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
Szczególnie często wyłącza się je z poziomu konstruktora kontekstu, szczególnie jeśli chcesz ustawić je w swoim rozwiązaniu:
public class MyContext : DbContext
{
public MyContext()
: base("MyContext")
{
Configuration.AutoDetectChangesEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
//snip
}
Praca z elementami pośredniczącymi
Powiedzmy, że mamy Product
i Category
w relacji wiele do wielu:
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; }
}
Jeśli chcemy dodać Category
do Product
, musimy załadować produkt i dodać kategorię do jego Categories
, na przykład:
Zły przykład:
var product = db.Products.Find(1);
var category = db.Categories.Find(2);
product.Categories.Add(category);
db.SaveChanges();
(gdzie db
jest podklasą DbContext
).
Spowoduje to utworzenie jednego rekordu w tabeli połączeń między Product
a Category
. Jednak ta tabela zawiera tylko dwie wartości Id
. Załadowanie dwóch pełnych podmiotów w celu utworzenia jednego małego rekordu to marnotrawstwo zasobów.
Bardziej efektywnym sposobem jest użycie encji pośredniczących , tj. Obiektów encji, utworzonych w pamięci, zawierających jedynie absolutne minimum danych, zwykle tylko wartość Id
. Tak to wygląda:
Dobry przykład:
// 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();
Wynik końcowy jest taki sam, ale unika się dwóch powrotów do bazy danych.
Zapobiegaj duplikatom
Jeśli chcesz sprawdzić, czy powiązanie już istnieje, wystarczy tanie zapytanie. Na przykład:
var exists = db.Categories.Any(c => c.Id == 1 && c.Products.Any(p => p.Id == 14));
Ponownie, to nie załaduje pełnych bytów do pamięci. Skutecznie odpytuje tablicę połączeń i zwraca tylko wartość logiczną.