Ačkoliv Entity Framework většina z vás zná a používá, dovolím si krátký úvod. Je jednou z vlajkových technologií pro vývoj v .NET Frameworku. Jedná se o ORM (mapper objektů na relační data) na steroidech, který dovoluje přistupovat, dotazovat a upravovat data z tabulek databází pomocí silně typových tříd (entity) a LINQ syntaxe (query). V ideálním případě vás plně odstíní od používání jazyka SQL a dovoluje pracovat s daty, jako kdyby se jednalo o kolekce přímo v .NET prostředí.
Mezi hlavní výhody řadím, krom právě silné typovosti a LINQ dotazů, sledování změn, lazy loading, migrace struktury databáze a její kontrolu a neposlední řadě projekci (lze provést projekční dotaz načítající jen požadované sloupce a to i napříč více “najoinovanými” tabulkami skrze cizí klíče).
Úskalí použití Entity Frameworku
Každá technologie má scénáře kdy se ji vyplatí používat a kdy naopak ne. Osobně EF velmi rád využívám a šetří mi mnoho času. Jsou ale projekty, kde ho buď použít nechci vůbec nebo jen ve velmi omezené míře. Berte proto následující seznam jako body k zamyšlení, zda je opravdu EF pro váš další projekt vhodný.
Rychlost
Rychlost (respektive pomalost) je jeden z častých argumentů právě proti EF. Sám jej nevnímám jako zásadní problém, je však dobré s tímto aspektem počítat.
První věc, která často vývojáře zarazí je problém rychlosti spuštění. Právě při prvním dotazu se EF může až na několik vteřin “zamyslet” – připravuje vnitřní struktury a kontroluje řadu věcí. Další dotazy již žádné takové zpoždění nemají. Pro naprostou většinu projektů je lehké prodloužení startu aplikace zanedbatelné.
Druhý a zásadnější problém je rychlost dotazů a aktualizace dat v databázi. Zpomalení zde může být znatelné, pokud načítáte a aktualizujete větší počty záznamů nebo větší objektové grafy. Samo o sobě je toto zpomalení pochopitelné, protože EF dělá řadu věcí, které prostě celý proces zpomalí.
Je dobré zmínit, že EF ve výchozím stavu jede v “plné palbě” a je možné ho zrychlit například vypnutím sledování změn a lazy loadingu (proxy tříd), pokud nehodláte načtená data modifikovat.
Osobně nevidím rychlost EF jako příliš velký problém. Je to prostě daň za pohodlnější a rychlejší vývoj (v některých případech), se kterou je potřeba počítat. Navíc lze často využívat hybridní přístup, kde EF používám na věci, kde výkonnostně stačí a na zbytek použiji přímo uložené procedury a ADO.NET nebo nějaký tenký mapper. Může jít například o hromadnou modifikaci mnoha řádků.
Jako vždy u rychlosti platí, že nejlepší je provést test rychlosti a pak teprve vynášet soudy a řešit další optimalizace.
Generované SQL
Pro databázové architekty může být nepříjemné, že dopředu neznají jaké dotazy se budou používat a jak budou sestavené – generuje je přímo EF a může se během vývoje rapidně měnit bez vědomí vývojáře. A s největší pravděpodobní budou dotazy velmi ošklivé. To je ovšem u systému jako EF pochopitelné. Nicméně jen hledání chyb v takovém rozsáhlém SQL není úplné příjemné.
Volba architektury
EF vždy pracuje v rámci Unit Of Work patternu (DataContext objekt), který reprezentuje logické připojení do databáze, sledování změn, které se propíší do databáze a je vstupním bodem pro sestavení dotazů. Změny na všech upravených objektech v rámci kontextu pak uložíte zavoláním SaveChanges metody. Poznámka: Vytvořený DataContext neznamená, že je automaticky otevřené spojení, to se otevírá a zavírá až v případě potřeby.
Při práce s EF máte v zásadě dvě možnosti. Buď použijete EF jen v rámci datové vrstvy jako pomocníka pro přístup k datům a vyšší vrstvy o něm již nebudou vědět – budou používat repositáře, commandy a různé pohledy. Druhá, rychlejší a jednodušší cesta je dát k dispozici DataContext všem, kdo jej potřebuje a budeme jej považovat přímo za datovou vrstvu. První postup se hodí na větší aplikace a naopak druhý je obvykle dostačující u jednodušších projektů. Samozřejmě rozhodující kritérium není jen velikost aplikace ale převážně požadavky na architekturu.
V dalších částech popisuji nevýhody jednotlivých postupů.
Nevýhody použití EF jako DL
Pokud se rozhodnete využívat přímo Entity Framework jako datovou vrstvu, můžete postupem času narazit na různé překážky.
Z architektonického pohledu je tu problém prakticky nulové abstrakce a testovatelnosti. Používáte přímo DataContext a jeho DbSet pro přístup ke všem tabulkám. Tyto objekty jde jen velmi těžko nahradit například za třídy pro přístup k jinému uložišti a prorostou nemalou částí aplikace. Zároveň může kterákoliv část aplikace používat přímo tabulky ze kterékoliv části databáze.
To má za následek často problém duplikace a rozrůstání databázových query napříč částmi aplikaci. Příkladem je načítání dejme tomu kategorií článků – 30 různých míst v aplikaci je potřebuje načítat a vy se rozhodnete přidat sloupec “IsDeleted” pro označení smazané kategorie, která se nemá zobrazovat. Vy tak následně musíte provést refactoring všech částí aplikace, které do tabulky kategorie přistupují. Čím bude podmínek více, tím komplexnější musíte rozšiřovat dotazy a tím větší pravděpodobnost chyby nastává. Navíc v praxi mohou být podmínky podstatně složitější a mohou prorůstat přes řadu tabulek.
Prorůstání IQueryable
Nepříjemným problém může být prorůstání IQueryable<Entity> napříč projektem. IQueryable je rozhraní popisující zdroj dat, nad kterým sestavujeme strom podmínek, řazení a projekcí a až při dotázání na data (foreach, ToArray apod.) se dotaz uskuteční. Dostat se k takové query můžete přímo přes DataContext přístupem k entitě nebo i přes lazy loading relací u entit (například article.Categories).
Teoreticky by nám mělo být jedno, zda je zdrojem dat přímo databáze nebo pole v paměti aplikace. Ve skutečnosti ale může být výsledek různý. Například u jednoduchého dotazu Where(u => u.Email == email) provedený proti poli v paměti bude výsledek rozlišovat velikost písmen (porovnání v .NET). Pokud jej provedeme proti EF tabulce, velikost písmen rozlišovat nebude (porovnání v SQL; záleží na nastavení sloupce v databázi). Reálný problém je pak situace, kdy část aplikace počítá s použitím přímo databázového query – často i nevědomě. Následně někdo provede refactoring a místo EF dotazu začne předávat běžné pole; na venek se vše tváří stejně, ale porovnávání začne rozlišovat velikost písmen. Takové problémy se hledají velmi špatně a prorostou celou aplikací.
Osobně mám nejradši, pokud IQueryable neopustí datovou vrstvu. Pokud potřebuji provádět filtrování, vytvořím si objekt popisující filtry a ten aplikuji na IQueryable přímo v datové vrstvě.
Lazy Loading
Lazy loading zajišťuje načítání relačních vazeb až v době, kdy k nim přistoupíme. Tato na jednu stranu užitečná funkce může způsobit i problémy.
Dejme tomu, že v databázové vrstvě přestanete načítat k entitě její relační vazbu (například article.Tags) a na prezentační vrstvě každý řádek zobrazení článku ukazuje i tyto tagy. Proto každý řádek provede lazy loading vlastnosti Tags. Z jednoho dotazu jich najednou bude v lepším případě třeba 50.
Toto je však obecně problém týkající se lazy loadingu. Výhodou je, že lze i v EF vypnout a vy tak snadněji zjistíte, že jste zapomněli načítat něco, co aplikace potřebuje.
Problém izolace repozitářů
Pokud se rozhodnete, že budete používat EF jen jako pomocníka v datové vrstvě, pravděpodobně vytvoříte pro přístup k datům repositáře pro jednotlivé entity – například repositář pro entity zákazníků a repositář pro entity faktur. Z pohledu repositáře je entita zákazníka samostatná – ukládáte ji samostatně bez ohledu na ostatní entity.
Pokud však používáte EF a v jedné části aplikace upravíte tabulku zákazníka v druhé části tabulku faktur, nemáte možnost, jak uložit jen entity například faktury. Zavolání SaveChanges uloží všechny změny v rámci DataContext.
Nejčastější řešení je volat SaveChanges ve všech write metodách repositářů aby nebylo možné opustit metodu bez provedení uložení. Problém nastává, pokud vývojář získá entity z repositáře, upraví je a nakonec se je rozhodne neuložit (například neprošla validace). Pokud nyní nic dalšího neprovede, entita se neuloží. Pokud však zavolá i naprosto nesouvisející repositář pro uložení úplně jiné entity, pak se uloží i rozpracovaná entita díky systému sledování změn.
I zde je možné tento problém vyřešit vypnutím automatických sledování změn a změny oznamovat manuálně v repositářích.
Mazání z vazebních tabulek
Entity se kterými pracujeme v rámci repositářů označujeme jako tzv. agreggate roots. Tedy minimální graf objektů, který načítáme/ukládáme. Například entita zákazníka po načtení z repositáře obsahuje i svoje adresy a zároveň při ukládání vždy ukládáme celého zákazníka i adresami.
Pokud v EF odstraníte entitu z kolekce relace (například customer.Addresses.Remove(address)), neprovede se její smazání – pouze dojde k rozvázání vazby (zde například mezi adresou a zákazníkem). Pro smazání adresy je nutné zavolat DeleteObject přímo na DataContext objektu. To bohužel znamená, že pokud chceme adresu smazat, nestačí ji jen smazat z entity zákazníka a zavolat SaveChanges ale musíme ale navíc přistoupit přímo k DataContext (od kterého by nás měl repositář odstínit).
Řešení této situace je trochu složitější. Je možné nahradit v entitách kolekce při načítání za vlastní, které sami sledují smazané položky a ty interně mažou i přímo na DataContextu.
Závěr
Vhodné postupy a problémy se obvykle liší projekt od projektu. Berte prosím mé názory jako doporučení k zamyšlení a ne jako dogmatický návod co je dobře a co je špatně. Bohu rád, pokud se se mnou podělíte o zkušenosti v diskusi.