Miroslav Holec
Premium

Rozdíly mezi voláním First, FirstOrDefault, Single, SingleOrDefault a Find v EF

Miroslav Holec   31. srpna 2015  update 29. března 2016

Tento článek je již zastaralý. Článek nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Minulý týden jsem se setkal se zajímavou otázkou, jaký je rozdíl mezi First, FirstOrDefault, Single, SingleOrDefault a Find metodách v Entity Framework. Osobně se ve většině aplikací setkávám s voláním FirstOrDefault, zatímco ostatní metody jsou ignorované. Je to dobře?

Pokusím se nyní popsat všechny scénáře, kvůli kterým existují různé metody pro získání jednoho záznamu z datové množiny.

Právě jeden záznam z množiny - Single

První scénář je jednoduchý. Potřebuji vrátit právě jeden jediný záznam a neočekávám (resp. je nežádoucí), aby takových záznamů bylo v databázi více nebo snad dokonce žádný. Příkladem může být tabulka s emailovými konfiguracemi. Já potřebuji výchozí (default) dle jména. Ta by tam měla být vždy a měla by být vždy právě jedna jediná.

Pokud není podmínka dodržena, pak je vyvolána výjimka. Dává smysl, že pokud konfigurace neexistuje, dál se zpracování zřejmě nehne a stejně tak pokud je záznamů více, nelze strojově správně rozhodnout, který záznam se má použít. Výjimka je tedy v tomto případě na místě.

Právě jeden záznam z množiny, pokud existuje - SingleOrDefault

První scénář se Single v tomto případě rozšiřuji o možnost, že záznam neexistuje. To už je velmi častá situace. Chci si například načíst uživatele dle emailové adresy. Takový uživatel může i nemusí existovat, ale je nemožné, aby v systému existoval dvakrát (pokud tedy předpokládám, že email je unikátní). Právě v takový moment použiju volání SingleOrDefault.

Pokud se nachází v množině více záznamů, které splňují dané pravidlo, pak je vyvolána výjimka. Opět by bylo totiž nesprávné strojově rozhodovat o tom, který záznam je ten správný. Evidentně taková situace neměla nastat a chod aplikace musí být zastaven.

První záznam z množiny - First

Metodu First použiju v případě, kdy jsem si jistý, že množina obsahuje vždy nějaké záznamy (počet > 0) a chci vrátit první záznam, který splňuje danou podmínku. Příkladem může být vrácení informace z tabulky historie, která zaznamenává chování uživatele. Předpokládám přitom, že se vznikem uživatele vzniká i první záznam (tedy alespoň jeden záznam v tabulce vždy bude).

Výjimka v tomto případě bude vyvolána pouze pokud neexistuje žádný záznam. V ostatních případech se mi vrátí první záznam z množiny, který splňuje daná pravidla. Často se First() používá i v kombinaci s metodami Where() a Order(). V praxi se volání First() nepoužívá příliš často, protože neexistující záznam obvykle ještě nemá zastavit běh programu a handlování výjimky by stálo zbytečný čas. Proto se používá poslední (velmi univerzální) možnost FirstOrDefault().

První záznam z množiny, pokud existuje - FirstOrDefault

Další scénář je nejběžnější. Vývojář chce získat první záznam z množiny dat, který splňuje určitou podmínku a nezáleží přitom, zda takový záznam existuje. V kódu se obvykle hned za tímto voláním objevuje kontrola, zda je vrácený záznam null či nikoliv. V souvislosti s počtem záznamů toto volání nikdy nepadá na výjimce. Příkladem může být načtení posledního článku autora blogu. Takový článek samozřejmě může i nemusí existovat.

Konkrétní záznam z množiny - Find

Poslední možností, která je často opomíjena je volání pomocí metody Find(). Je to za určitých okolností pátá možnost, jak vrátit konkrétní záznam z množiny. Používá se pouze nad DbSetem a to pro vyhledávání dle primárního klíče, případně složeného primárního klíče. K záznamu se dokáže Find dostat často rychleji, protože hledá nejprve pomocí StateManageru v paměti. Pokud záznam nenajde, pak se volá interně SingleOrDefault. Je to vcelku logické, protože při hledání dle primárního klíče se předpokládá, že záznam bude jeden nebo žádný.

K tématu Find ještě doplním, že podobnou metodu Find() lze najít i nad Listem. Tato metoda přijímá na rozdíl od výše uvedené predikát a vyhledává v záznam v kolekci v cyklu for. Hledání pomocí metody Find() s určitým predikátem je obecně rychlejší než alternativa s FirstOrDefault().

Výkonnostní rozdíly

Naznačil jsem, že metoda Find() má výkonnostní výhodu před ostatními zmíněnými metodami. Její nevýhodou je však flexibilita, protože ji vývojář nepoužije jinak než pro vyhledání záznamu dle primárního klíče. Kromě toho je metoda použitelná pouze nad DbSetem.

Jak je to s výkonností ostatních metod? Volání pomocí Single / SingleOrDefault je pomalejší než varianta First / FirstOrDefault. Pokud má totiž metoda Single ověřit, že vrácený záznam je právě jeden (a ne více), musí se pokusit záznamů najít více. V případě dotazování se vůči SQL serveru můžeme pozorovat dva rozdílné dotazy:

db.Articles.FirstOrDefault(x => x.ArticleId == 10)

exec sp_executesql N'SELECT TOP (1) 
    [Extent1].[ArticleId] AS [ArticleId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Description] AS [Description]
    FROM [dbo].[Articles] AS [Extent1]
    WHERE [Extent1].[ArticleId] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=10

db.Articles.SingleOrDefault(x => x.ArticleId == 10)

exec sp_executesql N'SELECT TOP (2) 
    [Extent1].[ArticleId] AS [ArticleId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Description] AS [Description]
    FROM [dbo].[Articles] AS [Extent1]
    WHERE [Extent1].[ArticleId] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=10

V tomto případě se z hlediska výkonnosti nic moc neděje. Za předpokladu vzdáleného připojení a většího množství sloupců v dotazované tabulce už ale musíme brát v potaz i přenesení dvojnásobného množství dat. To může způsobit výkonnostní potíže například při nesprávném použití lazy loadingu.

Data v paměti a zohlednění výjimek

První ukázka brala v potaz dotazování se vůči SQL serveru. Co když ale budeme pracovat s in-memory data?

Rozdíly First vs Default

Pak nezbývá volání Single(OrDefault) opět ověřit, že v kolekci neexistuje druhý záznam. Protože tomu tak většinou není, projde Single(OrDefault) celou kolekcí až na konec. Na příkladu výše je pak vidět, že zatímco FirstOrDefault() našel okamžitě požadovaný záznam (na začátku množiny) a vrátil jej, SingleOrDefault strávil dalších 17 milisekund zbytečným hledáním druhého záznamu.

Pokud by takový záznam našel, byla by vyvolána výjimka. Tím se dostáváme k druhému aspektu volby vhodné metody, kterým je handlování výjimek.

Check null vs SingleOrDefault

Chování metody SingleOrDefault můžeme velmi snadno napodobit tak, že si jednoduše zažádáme o dva záznamy a ověříme, zda náhodou opravdu dva neexistují. Pokud existují dva, můžeme tuto situaci vyřešit snadnou cestou (např.: zalogovat stav a dokončit běh programu s nějakým chybovým hlášením). Oproti tomu použití SingleOrDefault v chráněném bloku s cílem řídit dále chod programu je jak už to u zpracování výjimek bývá hodně drahé.

Závěr

Zkusme si shrnout ze všeho výše napsaného alespoň trochu logický postup při volbě vhodné metody.

  1. Pokud potřebuji z databáze získat záznam dle PK, dávám přednost volání Find(). Očekávám návrat 0/1 prvku a pokud by prvků bylo více (což je skoro vyloučeno), dostanu výjimku.
  2. Pokud potřebuji z databáze získat právě jeden záznam a vím, že kdyby jich bylo více, byl by to dost kritický průšvih, pak použiju Single nebo SingleOrDefault. Tyto metody vrátí výjimku, na které program lehne (uživatel uvidí error 500)
  3. Pokud potřebuji z databáze libovolný první záznam, který splňuje určitou podmínku, pak použiju First nebo FirstOrDefault
  4. Pokud potřebuji z databáze získat právě jeden záznam a vím, že když jich bude více, znamená to problém, který ale nechci řešit chybou 500 (budu dál řídit tok programu), pak zvážím standardní volání pomocí Where() v kombinaci s Take().

V naprosté většině případů jsem zastáncem dát přednost volání Find() nebo FirstOrDefault(), které dává prostor pro další rychlé zpracování běhu aplikace v různých situacích. Volání Single() a SingleOrDefault() doporučuji používat v těch místech aplikace, kde je neočekávaný výsledek zárodkem nějaké systémové chyby, která neměla nastat a zpracování nutně musí shořet na výjimce.