Jsou přínosy ORM nástrojů reálné?

Všichni víme že mezi relačním a objektovým modelem existují zásadní rozdíly - souhrnně označované jako "object-relation impedance mismatch." Pokud máme nad relačním systémem stavět objektový model (což je v případě objektového jazyka přirozené), musíme je nějak vyřešit, a ORM nástroje jsou jedna z možností. Ale jsou jejich často uváděné přínosy skutečné?

V podstatě všechny větší projekty na kterých jsem v poslední době pracoval ukládaly data do relační databáze s doménovým modelem implementovaným v Javě, přičemž k překonání rozdílů mezi relačním a objektovým modelem využívají ORM nástroj - Hibernate nebo iBatis. A nutno říct že ne vždy jde všechno tak hladce jak líčí reklamní sliby podomních prodejců ORM nástrojů ...

Podívejme se na několik často uváděných údajných výhod ORM nástrojů, a zkusme se zamyslet nad tím zda na reálných projektech a v dlouhodobém výhledu skutečně platí. V následuijícím textu bych rád polemizoval zejména s těmito čtyřmi (údajnými) positivními důsledky používání ORM nástrojů:

  1. zrychlení vývoje
  2. znatelná redukce kódu
  3. abstraktnější a jednodušeji portovatelný kód
  4. bohatý dotazovací jazyk

Chyták je v tom že všechny tyto přínosy tak trochu platí - alespoň na začátku projektu, kdy vám umožní rychlou implementaci základní verze projektu. Problémy s ORM obvykle začínají až v pozdějších fázích projektu když složitost projektu překročí určitou mez a když už je jen velmi obtížné rozhodnutí o použití ORM nástroje změnit.

Ale ani když už problémy skutečně přijdou, vývojáři (a management) se zuby nehty drží ORM protože přínosy jeho eliminace lze jen obtížně kvantifikovat zatímco náklady na jeho eliminaci lze většinou kvantifikovat velmi dobře. (Schválně zkuste jít za projektovým manažerem s návrhem "Chceme přepsat persistenční vrstvu - zabere nám to 100 človekodnů ale přínosy vlastně neumíme kvantifikovat," a uvidíte co vám řekne).

Vřele doporučuji článek Teda Newarda nazvaný Vietnam informatiky (The Vietnam Of Computer Science) - je sice starší (pochází z roku 2006), ale na platnosti mu to nic neubírá a poměrně pěkně shrnuje podstatu problému pomocí analogie s válkou ve Vietnamu. Nejasné či nesplnitelné a občas dokonce protichůdné cíle a nejasně formulované "exit podmínky," to jsou příčiny porážky USA ve Vietnamu a důvody proč tolik projektů trpí kvůli ORM.

A nyní už k jednotlivým výhodám, resp. údajným výhodám ORM.

Zrychlení vývoje

Je pravda že zejména v prvních fázích vývoje přináší ORM nástroje nepopiratelné urychlení - umožňují vygenerovat na základě metadat (definovaných např. anotacemi nebo v XML) základní CRUD metody a automatické mapování výsledků dotazů na doménové objekty.

Problém je že takto idylicky to v realitě funguje pouze v počátečních fázích vývoje, ale s tím jak se projekt zesložiťuje (a persistenční vrstva stabilizuje) se přínosy rozmělňují. Naopak řešení komplikací spojených s ORM zabírá čím dál více času - kolikrát už jste si říkali "kdybychom nepoužívali Hibernate vyřešil bych to v SQL za 5 minut?" A klidně se může stát že s pokračujícím vývojem tyto problémy spolykají násobně více času než se díky ORM na počátku projektu ušetřilo.

Příkladem často opomíjených praktických problémů je obtížné načtení změn v konfiguraci ORM jinak než kompletním restartem aplikace. Přidáte field do definice beany v XML (Hibernate) nebo změníte SQL dotaz v iBatisu, a pak musíte provést redeploy aplikace protože neexistuje jiný spolehlivý způsob jak ORM reinicializovat. V počátečních fázích je aplikace malá a její redeploy tak je téměř okamžitý, ale v pozdějších fázích může nabobtnat tak že redeploy zabere i několik minut. A když redeploy provedete několikrát denně ...

Znatelná redukce kódu

Často se jako velká výhoda některých ORM nástrojů uvádí redukce množství kódu, což samozřejmě úzce souvisí s předchozím bodem - a nutno říct že i v tomto případě je to do jisté míry pravda. Např. Hibernate umí na základě metadat (XML nebo anotací) automaticky mapovat výsledky dotazů na doménové objekty. Otázkou ale je zda vedlejší efekty ORM nástrojů nevedou ke zbytečnému zvýšení množství kódu na jiných místech.

Za příklad si můžeme vzít vlastnost "lazy-load" (v Hibernate), která umožňuje specifikovat kdy a jak mají být načítány části tříd (entit) ukládaných do databáze. Například pokud má entita jako property další entitu nebo kolekci entit, má být načtena hned při načtení hlavní entity nebo až když je poprvé použita? Hibernate toto řeší pomocí wrapperů namísto kolekcí a dynamických proxy tříd namísto jednotlivých entit (bytecode injection pomocí CGLIB), a umožňuje specifikovat spoustu parametrů na různých úrovních (jednotlivé properties nebo celé třídy) které zásadním způsobem ovlivňují výkon a škálovatelnost celé aplikace - mimo jiné se jedná o parametry lazy, fetch, batch-size a další.

Velice pěkné stručné shrnutí najdete v článku krátký úvod do strategií načítání, a poněkud podrobnější rozbor problémů spojených s lazy loadingem si můžete přečíst například v článku Hibernate: Understanding Lazy Fetching.

Ale zpět k problémům s lazy fetchingem - jestli jsem se za těch několik let ohledně výkonu databáze něco naučil, pak by se to zjednodušeně dalo shrnout do moudra "Vždy načítej jen ta data která potřebuješ, a dělej to v co nejmenším počtu dotazů" což se dá rozložit do dvou jednodušších pravidel:

  1. žádné zbytečné dotazy - pokud potřebujete data o více souvisejících entitách, neprovádějte samostatné selecty pro každou z nich ale zjoinujte si je a načtěte data jedním dotazem
  2. žádná zbytečná data - nenačítejte více dat než pro daný use case skutečně potřebujete

Jenomže právě toto je s lazy fetchingem velmi obtížně dosažitelné, protože instrukce, které položky načítat hned a které až později, jsou zakódovány v metadatech (ať už v anotacích nebo v XML).

Viděl jsem projekty ve kterých byl celý doménový model definován jako "silně lazy" - což bylo výhodné pro některé interaktivnější části (frontend), ale asi si dovedete představit jaký dopad to mělo například na batch processing, který mimochodem tvořil zcela zásadní část aplikace.

Setkal jsem se s různými bizarními řešeními tohoto problému - definicí více mapování pro jednotlivé persistentní třídy (každé s různou variantou lazy loadu) nebo "přípravný" průchod daty před vlastním zpracováním, jehož účelem bylo pouze načtení všech potřebných dat. To ale asi nepovede zrovna k redukci množství kódu, že?

Každopádně existuje jediné koncepčně a technicky správné řešení "lazy fetch problému" - pečlivě analyzovat jednotlivé use casy, určit která data skutečně potřebují, a zajistit aby se načetla právě a jen tato potřebná data (properties které daný use case používá). Pokud daný use case potřebuje výrazně jiná data než ostatní, bude nutno nadefinovat novou query (ať už HQL v Hibernate nebo SQL v iBatisu). Opět trocha kódu navíc ...

Abstraktnější a lépe portovatelný kód

Upřímně řečeno, není mi úplně jasné co je "abstraktnějším kódem" vlastně míněno. Při vývoji je postupně definován jednak doménový model, sestávající z definice objektů např. v Javě, a model persistenční (definice schématu v relační databázi). Úkolem ORM je "mapovat" tyto modely automaticky jeden na druhý, ale jejich tvorba je čistě na vás - stejně jako úroveň jejich abstrakce. Ano, ORM vám do jisté míry brání v páchání různých nepravostí jako je například míchání business a persistenční logiky, ale to není problém (s trochou vývojářské soudnosti) dosáhnout i bez ORM a s abstraktností to má společného jen pramálo.

Otázka portability je snad ještě pochybnější. Uvážíme-li totiž standardní aplikaci s třívrstvou architekturou (tj. aplikaci s persistenční, business a prezentační vrstvou), souvisí s ORM přímo jen persistenční a business vrstva (pokud máte přímé vazby mezi prezentační vrstvou a ORM, máte daleko zásadnější problémy než je portabilita). Přitom "portováním" rozumíme buď změnu databáze (v případě persistenční vrstvy) nebo programovacího jazyka (business vrstva).

Troufám si tvrdit že změna programovacího jazyka u větších projektů z důvodu nákladů víceméně vyloučená věc - náklady totiž vesměs výrazně převyšují potenciální přínosy takové změny. Navíc většina ORM nástrojů je určena pouze pro jeden či dva programovací jazyky, takže změna programovacího jazyka by v důsledku znamenala i změnu ORM (přepis konfigurace atd.).

Změna databáze už je o něco reálnější - zaprvé čas od času dochází k rozhodnutí sjednotit používané databáze, apod. ale také existují projekty které mají podporovat více databází současně (v podstatě balíkový software nasazovaný u více klientů). Je pravda že důsledným používáním ORM je programátor odstíněn od konkrétní implementace databáze, ale stejně tak je pravda že s tím jak během vývoje roste složitost projektu, objevují se oblasti na které dotazy generované např. Hibernate nestačí - patří mezi ně například reporting (agregované dotazy), apod.

V takových oblastech se standarně používá prosté SQL, spouštěné bokem "vedle ORM." Ale každý takový SQL dotaz z hlediska portability představuje problém - změny databáze může vyžadovat radikální přepis těchto SQL dotazů.

Stejně tak je pravda že takto důsledné používání ORM má často za následek eliminaci některých výhod dané databáze. Jednotlivé databáze sice všechny podporují SQL (byť s různými dialekty), ale kromě toho podporují i různé speciality za které se platí nemalé peníze.

Na druhou stranu v případě ORM řešení založených na čistém SQL (např. iBatis) o portabilitě nemůže být příliš řeč - ať chcete nebo ne, budete muset dotazy jeden po druhém projít a přepsat.

Bohatý dotazovací jazyk

Pokud se nejedná o ORM nástroj založený přímo na jazyce SQL (jako je například iBatis), musí nástroj podporovat nějaký způsob jak formulovat dotazy. Existuje několik variant ale asi nejběžnější a nejflexibilnější je vlastní dotazovací jazyk (např. HQL v Hibernate).

ORM nástroje postavené na vlastním dotazovacím jazyce ale už z principu mohou využívat jen podmnožinu SQL společnou pro všechny podporované databáze, což ale ve svém důsledku znamená že velká část unikátních vlastností vašeho databázového systému - které ho odlišují od ostatních databázových systémů a za které často platíte nemalé peníze - přijde víceméně vniveč.

Ano, je pravda že například již zmíněné HQL umožňuje poměrně komfortní navigaci objekty uloženými v databázi a mapování výsledků na objektový model, ale to z HQL ještě nečiní "bohatý dotazovací jazyk." Pokud si vezmete libovolný dotaz napsaný v HQL, celkem triviálně ho dokážete přepsat na SQL a relativně jednoduchým post-processingem výsledků namapovat na doménové objekty.

Závěr

 

Je mi jasné že po přečtení tohoto článku to může vypadat že jsem zatvrzelým odpůrcem ORM a že se řídím heslem "zahoďte ORM a pište si vše ručně," ale to je omyl způsobený tím že tématem článku je rozbor potíží souvisejících s ORM nástroji - odtud plyne negativní ladění článku.

Pravdou ale je že nic není dokonalé - ORM nástroje nevyjímaje, a to co se zdá výborné na začátku projektu může být v pokročilejších fázích vývoje kontraproduktivní. Stále sledujte zda je zvolené ORM řešení přínosem pro projekt, a pokud začne projekt brzdit neváhejte s přechodem na nějaké lepší řešení. Jak obtížná tato změna bude záleží jen na vás a na tom jak dokážete business a persistenční logiku oddělit.

Odkazy

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)