Slovenská verzeSK

Entity Framework a optimalizace dotazů

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í.

nenechte si ujít

Přednáška Testování REST API

🗓️ Online 10. května 2021, od 16 hodin

Jak lze testovat REST API na platformě .NET 5? Během přednášky si ukážeme aktuální trendy v testování aplikací od nástrojů pro psaní unit testů a integračních testů až po nástroje pro automatické testování a monitoring produkčních REST API.

Registrace zdarma

👨‍🎓 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í.

Miroslav Holec

Miroslav Holec

19. října 2015
upraven 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.
Loading