Lazy Loading jest wzorcem w którym pobieranie danych z bazy danych odraczane jest do momentu, gdy rzeczywiście te dane są nam potrzebne. W zależności od scenariusza możemy zyskać lub stracić na wydajności całej aplikacji. Z tego powodu rozwiązanie to jest opcjonalne w EF Core - wsparcie pojawiło się w wersji 2.1.
Zanim jednak porozmawiamy o różnych scenariuszach skupimy się na podstawowej funkcjonalności wzorca. Implementacja Lazy loading może być zrealizowana na dwa sposoby:
użycie Proxies;
wykorzystanie ILazyLoader.
Proxies
Proxies to obiekty pochodzące z encji, które generowane są w środowisku wykonawczym przez EF Core. Obiekty te mają zaimplementowane zachowanie, które powoduje, że zapytania do bazy danych w oparciu o referencyjne właściwości nawigacyjne wykonywane są na żądanie. Warto również mieć na uwadze, że był to domyślny mechanizm leniwego ładowania danych dla poprzedniej wersji Entity Framework. W naszym przypadku musimy wykonać trzy dodatkowe czynności:
Przygotowanie odpowiedniej konfiguracji na bazie metody UseLazyLoadingProxies():
// W przypadku aplikacji konsolowych opartych na .NET Core wykorzystamy metodę OnConfiguring():
protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
{
if (!optionBuilder.IsConfigured)
{
// Wymagana paczka: Microsoft.EntityFrameworkCore.Proxies
optionBuilder
.UseLazyLoadingProxies()
.UseSqlServer(@"Server=PAWEL;database=EFCoreCarDefintionDB;Integrated Security=true");
}
}
// W przypadku aplikacji ASP.NET Core posłużymy się metodą ConfigureServices():
services.AddDbContext<ApplicationDbContext>(options =>
options
.UseLazyLoadingProxies()
.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
Oznaczenie właściwości nawigacyjnych jako virtual - jest to kluczowy krok z perspektywy EF Core:
public abstract class Entity
{
public int Id { get; set; }
}
public class CarBrand : Entity
{
public string Brand { get; set; }
}
// Encję Fuel rozszerzmy o informację dotyczącą norm spalania
public class FuelType : Entity
{
public string Type { get; set; }
public virtual CombustionStandard Standard { get; set; }
}
public class CombustionStandard : Entity
{
public string Standard { get; set; }
}
public class Sales : Entity
{
public int Quantity { get; set; }
public virtual CarDefintion CarDefintion { get; set; }
}
public class CarDefintion : Entity
{
public string Model { get; set; }
// Referencyjna właściwość nawigacyjna
public virtual CarBrand Brand { get; set; }
// Referencyjna właściwość nawigacyjna
public virtual FuelType Fuel { get; set; }
// Referencyjna właściwość nawigacyjna
public virtual List<Sales> Sales { get; set; }
}
ILazyLoader
Drugim sposobem jest wstrzyknięcie usługi ILazyLoader. Jest to interfejs reprezentujący komponent, który jest odpowiedzialny za załadowanie właściwości nawigacyjnej jeżeli ta nie została jeszcze załadowana. Podejście takie pozwala na pominięcie generowania proxy. Interfejs ILazyLoader może być używany na dwa sposoby:
Pierwszy sposób to wstrzyknięcie interfejsu do encji głównej w relacji, gdzie jest używany do ładowania powiązanych danych. Podejście takie wymaga zależności Microsoft.EntityFrameworkCore.Infrastructure;
Drugi sposób to wykorzystanie delegata Action w celu wstrzyknięcia interfejsu.
Skupmy się najpierw na pierwszym podejściu. Poniżej lista niezbędnych kroków do przeprowadzenia poprawnej konfiguracji:
dodanie zależności Microsoft.EntityFrameworkCore.Infrastructure do przestrzeni nazw;
Dodanie pola dla instancji ILazyLoader;
Domyślny konstruktor oraz drugi przyjmujący ILazyLoader jako parametr;
Pole dla właściwości nawigacyjnej kolekcji;
Getter dla publicznej właściwości, który używa metody ILazyLoader.Load()
public class CarDefintion : Entity
{
// Wymagana paczka: using Microsoft.EntityFrameworkCore.Infrastructure;
private readonly ILazyLoader _lazyLoader;
public CarDefintion()
{
}
// Konstruktor przyjmujący ILazyLoader jako parametr
public CarDefintion(ILazyLoader lazyLoader)
{
_lazyLoader = lazyLoader;
}
// pole dla kolekcji właściwości nawigacyjnej
private List<Sales> _sales;
public string Model { get; set; }
// Referencyjna właściwość nawigacyjna
public CarBrand Brand { get; set; }
// Referencyjna właściwość nawigacyjna
public FuelType Fuel { get; set; }
// Referencyjna właściwość nawigacyjna
// Getter publicznej właściwości używający metody 'Leniwego Ładowania'
public List<Sales> Sales
{
get => _lazyLoader.Load(this, ref _sales);
set => _sales = value;
}
}
Proces konfiguracji dla pierwszego sposobu jest ukończony. Zanim jednak przejdziemy do analizy działania mechanizmu spójrzcie dla formalności na drugi sposób wstrzykiwania ILazyLoader:
public class CarDefintion : Entity
{
// Tym razem użyjemy delegata Action
private Action<object, string> _lazyLoader { get; set; }
public CarDefintion()
{
}
// Wstrzykiwanie delegata przez konstruktor
public CarDefintion(Action<object, string> lazyLoader)
{
_lazyLoader = lazyLoader;
}
// pole dla kolekcji właściwości nawigacyjnej
private List<Sales> _sales;
public string Model { get; set; }
// Referencyjna właściwość nawigacyjna
public CarBrand Brand { get; set; }
// Referencyjna właściwość nawigacyjna
public FuelType Fuel { get; set; }
// Referencyjna właściwość nawigacyjna
// Getter publicznej właściwości używający metody 'Leniwego Ładowania'
public List<Sales> Sales
{
get => _lazyLoader.Load(this, ref _sales);
set => _sales = value;
}
}
public static class PocoLoadingExtensions
{
public static TRelated Load<TRelated>(
this Action<object, string> loader,
object entity,
ref TRelated navigationField,
[CallerMemberName] string navigationName = null)
where TRelated : class
{
loader?.Invoke(entity, navigationName);
return navigationField;
}
}
Nie ważne czy wykorzystaliście proxies czy interfejs ILazyLoader - jesteśmy gotowi na szczegółową analizę działania mechanizmu.
Lazy Loading
Spójrzmy jak wygląda działanie lazy loading w praktyce.
using (var context = new ApplicationDbContext())
{
var cars = context.CarDefintion;
foreach (var car in cars)
{
Console.WriteLine($"Model: { car.Model}");
foreach (var sale in car.Sales)
{
Console.WriteLine($"Sprzedaż: {sale.Quantity}");
}
}
}
Pierwsze zapytanie pobiera wszystkie definicje samochodów. Niestety, wraz z kolejną pętlą w której sięgamy po powiązane dane, pojawia się n kolejnych zapytań do bazy danych. Jest to tzw. problem N+1 z którym napewno miałeś styczność – do bazy danych zostają wysyłane zbędne zapytania, które mogą spowodować problemy z wydajnością.
Generowane zapytania SQL można podejrzeć wykorzystując mechanizm logowania, który jest integralną częścią środowiska .NET Core. O szczegółach konfiguracji opowiem w osobnym wpisie – teraz jedynie podejrzymy (dodatkowe) zapytania będące efektem zastosowania mechanizmu lazy loading:
Ten sam zestaw wyników moglibyśmy otrzymać wykorzystując metodę rozszerzającą Include() o której szeroko pisałem w poprzednim wpisie: EF Core - zapytania
Ale jest też druga strona medalu…Wszystkie dane pobraliśmy w jednym zapytaniu. W międzyczasie doszło do edycji/dodania danych po stronie bazy danych – w przypadku takiego podejścia EF nie odswieży danych już pobranych a my będziemy widzieli tylko te przed aktualizacją.
Jednoznaczne stwierdzenie które podejście jest ‘lepsze’ nie jest zatem możliwe. Wszystko zależy od przypadku użycia a my powinniśmy być pewni, że zastosowanie lazy loading jest słuszne i uzasadnione.
Podsumowując: mam nadzieję, że teraz widzicie dlaczego ten mechanizm jest domyślnie wyłączony w Entity Framework Core. Wszystko związane jest z w pełni świadomym użyciem.