Miroslav Holec
Premium

Preloading dat v Entity Frameworku

Miroslav Holec   2. října 2016  update 21. září 2016

Tento článek byl napsán v roce 2016. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Entity Framework poskytuje tři cesty jak načítat související navigační property. V tomto krátkém přehledu shrnu rozdíly mezi jednotlivými přístupy a jejich vhodné použití v různých situacích.

👨‍🎓 Nové školení EF Core pro rok 2020

Školení nejnovější verze Entity Framework Core přímo u Vás ve firmě. Naučíte se používat EF Core v celém životním cyklu aplikace od vytvoření modelu, změn v podobě migrací až po dotazování a profilování databázových dotazů.

Více o školení Entity Framework Core

Problém načítání entit

Ve světě objektově relačního mapování má vývojář k dispozici možnost namapovat si související entity k libovolnému záznamu nebo kolekci dat. Na straně databáze se provede JOIN nad vybranými tabulkami a v aplikačním kódu jsou data následně zpřístupněna v čistě objektové formě. Pokud by vývojář napsal dostatečně komplexní dotaz, dokázal by snadno přenést celou relační databázi do paměti. Něco takového samozřejmě není žádoucí. Vývojář si vždy volí kompromis mezi určitým pohodlím a výkonností. Z hlediska výkonnosti je klíčová nejen komplexita dotazu, ale i množství přenášených dat po síti.

Komplexita dotazu

Mnoho vývojářů si dělá starosti s tím, zda jejich dotaz není příliš složitý. Přitom většina běžně vytvářených "složitých" dotazů nemá problém SQL server odbavit v řádu několika milisekund. Výjimkou jsou pouze případy, kdy EF vygeneruje mnoho vnořených dotazů například s IN a LIKE operátory nebo se vývojář snaží třídit a hledat ve velkých tabulkách se špatně nastavenými indexy.

Množství přenesených dat

I když SQL server dokáže leckdy vyrobit rozsáhlý JOIN během několika milisekund, je třeba si uvědomit, že součinem řádků z více tabulek může vzniknout velký balík dat. Ten je pak nutné po síti přenést, což nezřídka působí reálné výkonnostní potíže. Množství přenášených dat umí ukázat například EF Profiler. Důležité je také zohlednit cestu, kterou data putují a režijní náklady na zajištění takové cesty. Z logiky věci komunikace mezi SQL serverem a webovým serverem umístěným v jiné síti bude vyžadovat jiný přístup než situace, kdy je SQL server s webovým serverem na stejném stroji. Právě druhá zmíněná varianta je častou konfigurací stage prostředí ve vývojářských týmech a následné přenesení aplikace do odlišné produkční infrastruktury končí často fatálně.

Eager / Lazy / Explicit loading

Existují tři základní přístupy, jakými lze donačítat související data k entitám. Pokud máme objekt kategorie, můžeme k němu chtít donačíst například seznam článků. Ke každému článku můžeme chtít dále získat jeho autora, k autorovi počet článků atd. Načtení souvisejících dat je možné:

  • provést okamžitě = Eager Loading
  • nechat s odkladem na EF = Lazy Loading
  • provést později explicitně = Explicit loading

Lazy Loading

Během Lazy Loadingu necháme ORM, aby dle potřeby donačetl související entity. Prakticky tedy můžeme požádat o článek a během vykonávání kódu později donačteme autora (pokud je to potřeba).

article = context.Articles.FirstOrDefault();
model.Title = article.Title;

if(filter.ShowAuthorInfo)
{
    model.Author = article.Author; // odešle nový SQL query
}

Režim Lazy Loading je ve výchozím stavu povolen, případně jej můžeme povolit explicitně pomocí konfigurace DbContextu:

public class MyContext : DbContext
{
    public MyContext()
    {
        Configuration.LazyLoadingEnabled = true;
    }
}

Tato vlastnost contextu je přístupná po celý životní cyklus DbContextu a lze ji za běhu měnit. Není tedy problém Lazy Loading aktivovat jen za určitých situací (např.: některý aplikační modul, třídu, atd.).

Dále je nutné, aby navigační vlastnosti nad kterými chceme povolit Lazy Loading byly virtual a EF mohl tyto vlastnosti overridovat. Příklad budiž entita Article.

public class Article
{
    public int ArticleId {get; set;}
    public string Title {get; set;}
    public virtual Author Author {get; set;}
}

Na Lazy Loading lze nejlépe pohlížet jako na alternativní způsob donačtení entit ve chvíli, kdy z nějakého důvodu nestačí Eager Loading. Typickou situací je použití polymorfismu nebo sestavování dotazů u kterých předem nejsme schopni říct, jaká data budeme potřebovat. Vždy je ale nutné zvážit, zda chceme tuto odpovědnost EF předat, nebo raději načteme více (i nepotřebných) dat cestou o které víme, že bude vždy výkonnostně přívětivější.

Použití Lazy Loadingu typicky vede ke generování většího množství primitivních SQL dotazů s menším množstvím přenášených dat. Z principu věci tedy Lazy Loading obvykle nezpůsobí potíže, pokud je síťová cesta mezi relační databází a webovým serverem krátká (nejlépe stejný stroj).

Případů vyloženě špatného použití Lazy Loadingu je málo (většinou se jedná o fatální ponechání načítání entit v cyklech) a obvykle se tyto chyby projeví na výkonnosti a velmi snadno odhalí v každém profileru (formát několika po sobě jdoucích téměř stejných dotazů). Lazy Loading se nehodí používat také v případech, kdy vývojář předem předpokládá použití konkrétních dat ze souvisejících entit.

Na Lazy Loading doporučuji nepohlížet jako na nepřítele, kterého je třeba okamžitě eliminovat ale spíše na metodu, kterou je třeba pochopit, respektovat a ve správný moment využít.

Eager Loading

Eager Loading je explicitní požadavek na to, aby SQL server sestavil JOIN a vrátil všechna data, která vývojář potřebuje k práci. Z principu věci má Eager Loading vývojář k dispozici vždy - i se zapnutým Lazy Loadingem. Přístup Eager Loading umožňuje stavbu přesnějších dotazů a jejich lepší optimalizaci. Na druhou stranu rozsáhlé JOINy mohou způsobit potíže vygenerováním velkého objemu přenášených dat. Už proto by měla být součástí vytvářených dotazů dobře promyšlená projekce, která si vyžadá přenos pouze relevantních dat. Eager Loading provedeme použitím metody Include().

article = context.Articles.Include(x => x.Author).FirstOrDefault(); // vytvoří JOIN
model.Title = article.Title;

if(filter.ShowAuthorInfo)
{
    model.Author = article.Author; // žádný dodatečný SQL už není vytvářen
}

Explicit Loading

Třetí cestou donačtení entit je explicitní loading. Explicitní loading ponechává čistě na vývojáři, jaká související data chce načíst. Není přitom potřeba, aby byl zapnutý Lazy Loading a není potřeba, aby navigační properties byly virtual. Explicit Loading je v podstatě proces, kdy EF požádáme o sestavení dodatečného načtení dat a připojení těchto dat k datům, která se již nachází v DbContextu.

article = context.Articles.FirstOrDefault();
model.Title = article.Title;

if(filter.ShowAuthorInfo)
{
    context.Entry(article).Reference(p => p.Author).Load(); // odešle nový SQL query
    model.Author = article.Author;
}

Podobným způsobem by mohlo jít zpětně načíst kolekce dat pomocí

context.Entry(author).Collection(p => p.Articles).Load();

Za explicitní loading by bylo možné považovat i načtení dat například pomocí metody Find. Cizí klíč totiž známe.

article = context.Articles.FirstOrDefault();
model.Title = article.Title;

if(filter.ShowAuthorInfo)
{
    model.Author = context.Authors.Find(article.AuthorId); // odešle nový SQL query
}

Shrnutí a závěrečné poznámky

Neexistuje univerzální způsob, jak související data k entitám donačítat. Každý přístup má své výhody a nevýhody v závislosti na aplikační infrastruktuře a na logice, s jakou chceme data konzumovat. Pokud s Entity Frameworkem začínáte, doporučuji si všechny přístupy vyzkoušet v praxi. A pokud chcete do Entity Frameworku rychle proniknout, přijďte na mé listopadové školení.