Entity Framework a optimalizace dotazů
Tento článek byl napsán v roce 2015. 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.
Přestože Entity Framework značně usnadňuje práci s relační databází, existuje řada témat, která souvisí s výkonnostním hlediskem a která by měl vývojář při implementaci EF znát. Pokud Vás trápí výkonnost Vašeho řešení, série článků Entity Framework Performance Tuning by Vám mohla pomoci. V tomto článku je řeč o správě DbContextu a samotném dotazování.
👨🎓 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
Zbytečné odesílání dotazů do databáze
Při použití ADO.NET byl vývojář zvyklý explicitně otevírat a zavírat spojení do databáze. V případě Entity Frameworku se zkrátka vytvoří DbContext, předá se mu connection string (není nutné) a o vše ostatní se stará EF sám. Připojení se navazuje například při použití metod SaveChanges()
, Refresh()
, FirstOrDefault()
, First()
nebo Load()
. Z logiky věci tedy vyplývá, že tyto metody potřebujeme volat jen pokud to skutečně potřebujeme. Volání těchto metod speciálně v cyklech bývá zpravidla (ne vždy) výkonnostní průšvih.
PŘED
foreach(var article in articles) { context.Articles.Remove(article); context.SaveChanges(); }
PO
foreach(var article in articles) { context.Articles.Remove(article); } context.SaveChanges();
Voláním SaveChanges()
až po hromadném provedení změn obvykle vede k tomu, že EF se sám pokusí optimalizovat počet dotazů do databáze na minimum.
Vytváření DbContextu
Samotný DbContext je z řady důvodů (např.: konzistence dat) vhodné držet per web request. Je silně podezřelé, pokud v rámci webového requestu vznikají dva nezávislé DbContexty, ve kterých se odehrávají různé změny. Na druhou stranu i vytváření neustále dokola stejného DbContextu
v rámci jednoho requestu vede k výkonnostním problémům v souvislosti s managováním contextu (jeho vytvoření není triviální). Jak tedy na to?
Pomoci nám může libovolný IoC kontejner, s jehož pomocí vyrobíme DbContext za běhu a poté jej už pouze předáváme instancím, které jej potřebují. Pokud nepoužíváte IoC, pak je možné vytvořit DbContext
na jiném vhodném místě a pak jej dle potřeby předávat. V MVC aplikacích by takovým místem byl Controller
.
PŘED
List<Article> articles = new List<Article>(); using(var context = new MyContext()) { context.Articles.AddRange(context.Articles.Where(x => x.AuthorId == 5).ToList()); } ... using(var context = new MyContext()) { context.Articles.AddRange(context.Articles.Where(x => x.AuthorId == 7).ToList()); }
PO
public class MyClass { private readonly MyContex context; public MyClass(MyContext context) // injection { this.context = context; } } ... // uvnitř metody List<Article> articles = new List<Article>(); context.Articles.AddRange(context.Articles.Where(x => x.AuthorId == 5).ToList()); ... context.Articles.AddRange(context.Articles.Where(x => x.AuthorId == 7).ToList());
Ignorace IQueryable a duplikace dotazů
Příklad výše ale není stále dostatečně optimální, protože do databáze odchází dva dotazy. V rámci zpracování je možné použít IQueryable
a ponechat si odeslání SQL dotazu až na samotný konec..
SLOŽENÍ DOTAZU
List<Article> articles = new List<Article>(); int firstAuthor = 5; ... int secondAuthor = 7; articles = context.Articles.Where(x=> x.AuthorId == 5 || x.AuthorId == 7).ToList();
Výhoda IQueryable
je však lépe vidět při použití podmínky / klauzule WHERE
:
SLOŽENÍ DOTAZU S WHERE
public List<Article> GetArticles(int authorId, string contains = "") { var articles = context.Articles.Where(x=> x.AuthorId == authorId); if(!string.IsNullOrEmpty(contains) { articles = artcles.Where(x => x.Title.Contains(contains) || x.Description.Contains(contains)); } return articles.ToList(); // odeslani SQL dotazu }
Pokud by se již před podmínkou IF volala metoda ToList()
, automaticky by se provedlo odeslání SQL dotazu a v rámci těla podmínky by pouze došlo k výběru vhodných záznamů in memory. Jinými slovy dokud je možné dotaz zpřesňovat a samotná data nejsou fyzicky potřeba, měl by si vývojář vystačit s rozhraním IQueryable.
Zbytečné načítání nepotřebných dat
Technicky vzato ani výše uvedený příklad nemusí být stále optimální, pokud článek obsahuje mnoho vlastností (db atributů). Pokud potřebuje totiž vývojář pracovat jen s určitou množinou vlastností, není generovaný dotaz optimální:
SELECT * FROM Articles WHERE ...
Speciálně při práci s databází, která je mimo webový server následně dochází ke zcela zbytečnému přenosu enormního objemu dat. SELECT *
je šeredná praktika, na kterou jsou vývojáři upozorňováni prakticky neustále snad od středních škol. Příklad tedy můžeme upravit a použít DTO pro optimalizaci přenosu dat:
public List<ArticleDto> GetArticles(int authorId, string contains = "") { ... return articles.Select(x => new ArticleDto { Title = x.Title, Description = x.Description }).ToList(); }
Samotný dotaz je pak podstatně jednodušší:
SELECT Title, Description FROM Articles WHERE ...
Aktualizace se zbytečným přednačtením
Přestože často je potřeba s entitami pracovat a poté je uložit, někdy je jejich přednačítání vyloženě zbytečné a dá se mu efektivně vyhnout.
PŘED
var myArticle = context.Articles.Find(154); // SELECT * FROM articles ... myArticle.Title = "I just wanna change the title"; context.SaveChanges(); // UPDATE articles SET ....
PO
var myArticle = new Article() { ArticleId = 154 }; myArticle.Title = "I just wanna change the title"; context.Entry(myArticle).State = EntityState.Unchanged; context.SaveChanges(); // UPDATE articles SET ...
V prvním případě se nejprve načte objekt pomocí SELECT
dotazu a po aktualizaci proběhne UPDATE
. V případě druhém již nedochází k přednačtení dat ale rovnou k samotné aktualizaci. Článek je samozřejmě rozeznán na základě primárního klíče. Předpokladem pro tuto konstrukci je již existující záznam v databázi.
Řada vývojářů používá pro aktualizaci vs. vložení nového záznamu jednoduchý pattern:
public void InsertOrUpdate(Article article) { using (var context = new MyContext()) { context.Entry(article).State = article.ArticleId == 0 ? EntityState.Added : EntityState.Modified; context.SaveChanges(); } }
Odstraňování se zbytečným přednačtením
Problém odstraňování dat je zcela analogický k ukázce aktualizace. Pokud neexistuje vyloženě dobrý důvod proč by vývojář chtěl před odstraněním s entitou pracovat (např.: kontrola navázaných objektů atd.), pak není důvod ji přednačítat. Použít lze pro změnu metodu Attach()
.
PŘED
var myArticle = context.Articles.Find(154); // SELECT * FROM articles ... context.Remove(myArticle); context.SaveChanges(); // DELETE FROM ...
PO
var myArticle = new Article() { ArticleId = 154 }; context.Articles.Attach(myArticle); context.Articles.Remove(myArticle); context.SaveChanges(); // DELETE FROM ...
O odstraňování dat v Entity Frameworku jsem napsal už v minulosti samostatný článek.
Ignorace In-Memory entities s metodou Find()
Metoda Find()
vyhledává nejprve data v paměti a teprve pokud je nenajde, dochází ke komunikaci s databází. V případě Where()
nebo FirstOrDefault()
dochází k volání do databáze vždy.
PŘED
var myArticles = context.Articles.Where(x => x.ArticleId > 100).ToList(); // SELECT * FROM Articles var myArticle = context.Articles.FirstOrDefault(x => x.ArticleId == 154); // SELECT * FROM Articles WHERE ...
PO
var myArticles = context.Articles.Where(x => x.ArticleId > 100).ToList(); // SELECT * FROM Articles var myArticle = context.Articles.Find(154);
Pokud nemáte mezi metodami First()
, FirstOrDefault()
, Single()
atd. jasno, napsal jsem článek, kde tyto metody porovnávám i z hlediska výkonnosti.
Závěr
V tomto článku jsem popsal základní praktiky při dotazování se na data s použitím Entity Frameworku. Pokud Vás tento článek zaujal, těšte se i na další navazující články, kde budu popisovat nastavení DbContextu a další pokročilé techniky pro zvýšení výkonnosti aplikací.