Wprowadzenie

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:

  1. Pobranie odpowiedniej paczki: Microsoft.EntityFrameworkCore.Proxies;
  2. 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")));
    
  3. 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:

  • Instalacja paczki Microsoft.EntityFrameworkCore.Abstraction;
  • 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;
	}
}

Powyższy przykład nie jest jeszcze kompletny. Brakuje nam implementacji metody Load(). Tutaj jednak możemy zasięgnać pomocy i skorzystać z oficjalnej dokumentacji dostępnej pod tym adresem: https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy

Poniżej implementacja brakującej metody:

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: Lazy loading N+1

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.