Miroslav Holec

Miroslav Holec { Software Architect & Dev Evangelist }

miroslavholec.cz / blog / hrozba-jmenem-client-evaluation-v-entity-framework-core

Hrozba jménem Client Evaluation v Entity Framework Core

Miroslav Holec

Miroslav Holec

od EF Core 1.0.0 Publikován 2. května 2018 | ASP.NET Core & EF Core

Jedna ze zajímavých funkcí, které přináší EF Core je tzv. Client vs. Server evaluation. Krom toho, že funkce je velmi užitečná a určitě najde své uplatnění, její nepochopení a neuvážené použítí zcela určitě způsobí v mnoha aplikacích výkonnostní potíže. Pokud jim chcete předejít, přečtěte si tento článek, kde riziko objasním.

.NET Core

Školení Entity Framework Core

Praktické školení pro vývojáře, během kterého se bude mít každý příležitost seznámit se základními principy, modelováním, toolingem, optimalizací dotazů a praktickým použitím v moderních webových aplikacích.

Pracujete-li delší dobu s Entity Framework 6.x, zajisté jste se setkali s chybou ve smyslu:

LINQ to Entities does not recognize the method 'xxx' and this method cannot be translated into a store expression.

Chyba upozorňuje na snahu předat SQL Providerovi nesplnitelný úkol v podobě překladu LINQ dotazu na dotaz databázového úložiště. Většina vývojářů při prvním setkání s touto chybou dlouze bádá, ale časem pochopí příčinu a dokáže se této chybě vyhnout. Příkladem může být dotaz:

var products = db.Products
    .OrderBy(x => x.Price)
    .Select(x => new
    {
        x.Title,
        x.SeoLink,
        Price = UpdatePrice(x.Price)
    })
    .Take(2).ToList();

Nejenže s Entity Framework Core už nevyhučíte na výjimce, ale dokonce dostanete správné a očekávané výsledky.

Client vs. Server Evaluation

DB Provider nově dokáže vyhodnocovat část LINQ dotazu na straně databázového serveru a část na straně klienta. Příklad uvedený nahoře by byl přeložen do klasického SELECT dotazu (upraveno pro čitelnost):

SELECT TOP 2 [x].[Title], [x].[SeoLink], [x].[Price]
FROM [Products] AS [x] ORDER BY [x].[Price]

Z SQL dotazu je patrné, že SQL server vrátí všechna potřebná data na klienta a zbytek, tedy provedení metody UpdatePrice() už se děje na klientovi s využitím vlastnosti Price. Vše vypadá jednoduše, ale musíme mít napaměti, že SQL Server vyhodnotí a vrátí data tak, aby s nimi klientská strana mohla dále pracovat. A tady si lze snadno naletět.

V příkladu výše probíhá řazení dle "surové" ceny, což bez problémů dokáže SQL Server. Proto provede řazení, vrátí 2 záznamy a na klientovi se pouze vlastnost Price nastaví na základě výstupu metody UpdatePrice().

Stačí ale drobný přehmat a vše je jinak:

var products = db.Products
    .Select(x => new
    {
        x.Title,
        x.SeoLink,
        Price = UpdatePrice(x.Price)
    })
    .OrderBy(x => x.Price)
    .Take(2).ToList();

Drobná změna v podobě přesunu OrderBy() již generuje o poznání nebezpečnější dotaz, ve kterém už není omezení na první 2 záznamy:

SELECT [x].[Title], [x].[SeoLink], [x].[Price]
FROM [Products] AS [x]

V tomto případě totiž chceme provést řazení až na základě toho, co vzejde z metody UpdatePrice(). Databázový server logicky nemůže takové řazení provést a proto SQL Provider zažádá zcela o všechna data ze serveru, aby nad nimi mohla být provedena metoda UpdatePrice() a teprve následně Take().

Stejné chování by nastalo, pokud by vývojář například použil Where(), v jehož podmínce by bylo něco, co databázový server nedokáže vyhodnotit.

Na první pohled vypadá všechno logicky (a logické to je), nicméně když neznalý vývojář přijde k existujícímu kódu s úkolem nastavit řazení záznamů a v dobré víře vrazí OrderBy() na konec dotazu (což je u EF 6.x standardní praktika), vytvoří v případě tabulek s větším objemem dat výkonnostní problém.

Zakázání Client Evaluation

Máte-li z potenciální hrozby strach, můžete tuto feature zakázat a dotazy sestavovat starým způsobem. Opět bude na vás, abyste se rozhodli co dáte za úkol SQL Serveru a co si přepočítáte na klientské straně ve své aplikaci. Funkci je možné zakázat v DbContextu v metodě OnConfiguring():

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    .......
    optionsBuilder.ConfigureWarnings(w => w.Throw(RelationalEventId.QueryClientEvaluationWarning));

    base.OnConfiguring(optionsBuilder);
}

Jakmile se pokusí kdokoliv o Client Evaluation, dojde k vyhození výjimky:

Vyhození výjimky po přenastavení warningů

Pokud samozřejmě pracujete v menším týmu vývojářů a máte toto chování neustále na mysli, pak nemá smysl funkci vypínat a můžete ji ke své radosti využívat. V případě aplikací, kde si nelze podobné chyby dovolit nebo ve větších týmech s juniornějšími členy bych doporučil jít raději tradiční cestou.

Zeptejte se