Proč nemám rád Entity Framework

Tomáš Jecha       5. 5. 2016             4309 zobrazení

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.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Jaká alternativa?

Mám s EF podobné zkušenosti, jako Vy. Chtěl bych se tedy zeptat, co používáte místo něho?

Na standardním ADO.NET mi vadí nutnost vše ručně načítat do objektů a také nutnost ruční implementace stránkování či projekce.

Máte tipy na nějakou alternative/nadstavbu ADO.NET která by tyto procesy automatizovala, nebo dokonce generovala dotazy pomocí LINQ?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Zdravím,

používáme toto https://github.com/igor-tkachev/bltoolki... a zkušenosti velmi dobré. Jediná nevýhoda je, že už je to trochu letité a komunita už vázne. Což nám moc nevadí, máme to již zažité a děláme si i vlastní úpravy a rozšíření.

Druhý FW od stejného autora ve kterém jak píše vylepšil návrh původního BLToolkitu, ale ještě jsem detailně nezkoušel, ale je to velice podobné. Vzhledem ke kvalitám prvního FW bych tomu docela věřil.

https://github.com/linq2db/linq2db

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Mluvíte mi z duše

Mám velmi podobné zkušenosti s podobným heavy frameworkem - NHibernate s tím, že jsem po po této zkušenosti velmi dogmatický a těžké frameworky mi do projektů nesmí.

Použili jsme NHibernate v rámci docela velkého projektu kde se množství dat v tabulkách pohybuje v řádech desítek až stovek miliónů a časem se nám to velmi vymstilo. Bylo to způsobeno jak počáteční nezkušeností a nadšením jak to "celé funguje skvěle samo". Nadšení nás ale posléze přešlo v okamžiku kdy se začaly vynořovat výkonnostní a jiné problémy.

Vůbec teze skrýt před programátorem DB a dělat, že tam vlastně vůbec není považuji za jeden z největších omylů těchto FW. Ano funguje to na jednoduché aplikace s pár tabulkami a se stovkami řádků v nich. Tam je to opravdu fuk. Ale upřímně k čemu je programátor, který pořádně ani neumí napsat SQL dotaz aby nenačetl půlku DB :-) Z toho pak vycházejí další problémy, které zmiňujete. Neoptimalizované a špatně čitelné dotazy, které to generuje. Právě NHibernate na to byl expert, naprostá nečitelná hrůza v jednom řádku, něco v tom najít a zjistit proč je ten dotaz tak pomalý bylo peklo. A to co generoval Entity Framework v prvních verzích, to bylo opravdu něco.

Pokud programátora donutíte se nad tím dotazem zamyslet, než ho tam namastí, jak by mohl ho napsat aby dotaz použil existující indexy, jestli opravdu potřebuje všechny sloupce atd, je si myslím velice užitečné. Neříkám, že každý programátor má být zároveň DB specialistou, ale základní znalosti by mít měl. A právě skrytí DB těžkým frameworkem toto hatí.

Poslední rána je příchod nového člena do týmu, který nezná záludnosti použitého FW a díky špatnému mapingu, kaskády a lazy loadingu zjistíte, že máte v paměti půlku DB :-)

Z toho plyne moje poučení které s oblibou říkám: Tězký ORM FW je velmi jednoduché špatně použít.

Po několika pokusech a zkoumání jsme zůstali u FW BLToolkit. Má plně implementovanou podporu linqu včetně zajímavých extension, nehraje si na blackbox a DB neskrývá. Žádné sledování změn, žádný lazy loading, žádné kaskádové update a delete. Je to sice trochu pracnější na vývoj, ale dotazy to generuje hezké a čitelné a většinou je není ani potřeba přepisovat do čistého SQL. A v konečném důsledku čas ušetříte.

Od té doby na nás náš DB specialista neřve co to tam pouštíme za strašné hrůzy :-)

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Zajímavý článek

Hezky napsané, přidám pár svých postřehů.

Rychlost:

S rychlostí mám též občas problémy a ne jednou to dopadne tak, že je lepší napsat si čístý SQL nebo storku a tu pak přes ExecuteQuery zavolat.

Prorůstání IQueryable

S tím mám taky problém. Částečně ho může řešit vhodná architektura (nějaký object-query pattern), kdy každý dotaz se zabalí do třídy a vlastně ven se vůbec IQueryable nebude publikovat. Problém je, že některé komponenty a prvky (webapi) queryable vyžadují na vstupu. (Třeba kendo grid) a zatím se mi nepodařilo tuto vrstvu skrýt. Ale v jedné starší aplikaci sme narazili na přesně to samé. A nakonec jsme skončili na tom, že jsme si udělali alespoň extensions pro každou entitu.

Lazy loading:

Podle mě přináší víc problémů než užitku. Ve svých aplikací ho explicitně vypínám a tam kde potřebuji donačíst externí data používám Include. Má to tu výhodu, že mě aplikace sama upozorní (null reference) když zapomenu donačíst potřebná data. Nevýhoda je, že člověk často skončí s velkým includem a tam je ten výkon pak taky omezený. Při zapnutém Lazy loadingu jsme často museli hledat profilerem a koukat, kde nám unikají dotazy (SELECT N+1)

Jinak za mě dva větší problémy:

Značná paměťová náročnost. Zvlášť když aplikace běží na sdíleném webhostingu a má omezené množství paměti RAM k dispozici.

Podpora hierarchické struktury. Složitější stromové struktury je potřeba řešit buď přes čisté SQL (Sql CTE) nebo se smířit s tím, že se načtou všechny záznamy a traverzování se provádí až v paměti.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Myslím, že v těch bodech se shodneme, v zásadě jsem i to samé psal v článku, jen jinými slovy.

S pamětí může být problém, pokud načteme mnoho záznamů najednou a nevypneme u nich proxy třídy / sledování. Jinak jsou to jen hloupé objekty a paměť neplýtváme.

K té hierarchii je to možná dobře. Databáze má ukládat "jednoduchá data" a nějaké rozhraní nad tím si zařídím v business vrstvě.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Aplikace Vás "upozorní" až za běhu s tím, že spadne a to ještě jen tehdy, když zapomenete načíst obyčejnou property. Pokud někde zapomenete načíst kolekci, tak nic nespadne, ale v aplikaci začnou zdánlivě záhadně mizet a objevovat se data.

Já nechávám lazy loading implicitně zapnutý a Includy většinou přidávám až nakonec, v rámci optimalizace výkonu. Výhodou tohoto přístupu je, že aplikace nikdy nespadne kvůli tomu, že jsem někde zapomněl Include.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

EF a fulltext

Bezvadný článek, jdu si ho vytisknout do tramvaje.

K záporům EF bych ještě přidala, že neumí pracovat s fulltextovým indexem …

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Fulltextové rozšíření - pravda. Také velký nedostatek.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback