Miroslav Holec
Premium

Entity Framework a optimalizace dotazů

Miroslav Holec   19. října 2015upd. 29. března 2016

Tento článek je již velmi zastaralý. Článek 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í.