Rád bych zde poděkoval Ing. Petru Jedličkovi, Ph.D. za jeho vedení, cenné rady a zkušenosti při realizaci této práce, svým rodičům za podporu během mého studia a své přítelkyni za její trpělivost v době, kdy jsem tuto práci psal.
5
Abstract Kotrla, T. Possible technologies of working with databases in Java. Diploma thesis. Brno, 2009. This text is oriented to possible technologies of persisting object from Java language to database systems. After analyzing of possibilities, specifying the comparative criteria and presenting the specification of the sample example, three technologies of persisting to relation database management systems are explored and compared. They are JDBC, iBATIS and object relation mapping framework Hibernate. After presenting and comparing the most suitable technology is recommended for using on the specific types of project. Keywords: Java, persistence, JDBC, iBATIS and Hibernate
Abstrakt Kotrla, T. Možnosti práce s databází v jazyce Java. Diplomová práce. Brno, 2009. Práce se zabývá možnými způsoby perzistence objektů z programovacího jazyka Java do databázových strojů. Po analýze možností, určení srovnávacích kriterií a zadání vzorového příkladu zkoumá a porovnává tři vybrané technologie pro perzistenci do relačních databází. Jsou to JDBC, iBATIS a objektově relační mapovací rámec Hibernate. Po představení a srovnání je k použití na různých typech projektů doporučena nejvhodnější technologie. Klíčové slova: Java, perzistence, JDBC, iBATIS a Hibernate
Výpočetní stroje mají za sebou dlouhou historii a stejně tak i způsob zadávání povelů pro jejich práci. Pokrok v případě strojů znamenal jejich postupné zrychlování (počet provedených instrukcí za jednotku času), zmenšování, zlevňování, čímž se zvýšila dostupnost a došlo tak i k masivnímu rozšíření. Povely (v IT oblasti častěji nazývané jako instrukce) pro výpočetní stroje od dob děrných štítků zaznamenaly také značný rozvoj, od instrukcí a různých assemblerů se programovací jazyky přes strukturované (procedurální) dostaly až k objektovým. Rozvoj v oblasti programovacích jazyků tak umožňuje programátorům se plně soustředit na řešenou problematiku a zkracuje čas potřebný k vytvoření výsledného díla. Mezi objektové programovací jazyky patří i Java, v současné době oblíbená a ve velké míře rozšířená platforma. Noví programátoři si dokáží tento jazyk rychle osvojit, syntaxe je podobná jiným jazykům, existuje řada kvalitních učebnic a dokumentace v podobě JavaDoc je přehledná, dobře distribuovatelná a snadno dostupná. Pro reálné použití na komerčních projektech nebo i pro rozsáhlejší školní projekty však základní znalosti, jako syntaxe, nedostačují. Vývojáři musí řešit složitější problémy jako trvalé uložení dat (perzistenci) nebo komunikaci s jinými systémy. Pro taková řešení je nutné dobře znát možnosti jazyka a dostupné nástroje. Perzistence je typický úkol, který je nutné při vývoji systémů řešit, neboť málokterá aplikace nepotřebuje svoje data uchovat do příštího běhu. V jazyce Java existuje API pro převod vnitřního stavu objektů do binárního toku, který lze pomocí jiného API zapsat jako soubor na souborový systém. Řešení popsané v tomto odstavci ale trpí řadou nedostatků. Binární reprezentace je zpětně zpracovatelná pouze v Javě, což v některých případech může omezit použitelnost uložených dat. V případě potřeby lze tento nedostatek řešit použitím nativní textové nebo XML reprezentace stavu objektů místo binárního. Navíc jsou tímto způsobem data dostupná jen na jednom místě. Tím je disk počítače, kde aplikace běží, přístup k datům i pro uživatele jiných počítačů se tím komplikuje. Problém s dostupností je možné vyřešit použitím síťového disku nebo sdílením lokálního úložiště prostředky operačního systému, případně složitěji pomocí implementace vlastního proprietárního sdílení. Při typickém využití chce uživatel navíc vyhledat právě některé údaje. Java sama o sobě nemá efektivní způsoby pro ukládání a prohledávání většího množství dat. Zpracování uživatelských dotazů jen prostředky Javy není lehce řešitelné. Není k tomu ani důvod, protože tento problém efektivním způsobem řeší databázové systémy, kde se data udržují centralizovaně a zpřístupňují procesům běžícím i na jiných počítačích. Takové systémy často lze škálovat a dosáhnout tak při větší zátěži požadovanou dostupnost.
1.2
Cíl práce
8
Programátoři se tak v Javě starají o business logiku a realizují prezentační vrstvu. O uložení a zpracování dat se naopak starají databázové systémy. Právě porovnání různých způsobů propojení Javy a relačních databázových systémů pro účely perzistence řešené v perzistentní vrstvě aplikací je hlavním tématem této práce.
1.2
Cíl práce
Cílem této práce je podpora rozhodování o způsobu perzistentního ukládání dat v aplikacích vyvíjených v programovacím jazyce Java. Pro rozhodnutí bude nutná analýza dostupných možností, bude jí věnována následující kapitola 2. Během analýzy je nutné vyřešit způsoby perzistence stavu programu a trvalé uložení jím zpracovávaných dat. V rámci perzistence dat a stavu bude potřeba analyzovat i používaná úložiště. Následně bude možné u nejrozšířenějších způsobů a úložišť identifikovat různé problémy s jejich použitím. Závěrem analýzy by měl být seznam používaných technologií pro perzistenci. Následující kapitola 3 se bude věnovat použité metodě při řešení cíle této práce. Objektivní srovnání a ohodnocení možností perzistence je možné jen na základě předem definovaných kritérií. Volba takových kritérií a jejich aplikace při porovnávání možností je nutnou podmínkou pro splnění hlavního cíle této práce. Mezi cíle této práce však patří i možnost využít její části jako studijní pomůcku pro akademickou obec. Praktické ukázky ulehčují a urychlují studium, pro tyto účely bude tedy v práci opakovaně řešen jeden vhodný vzorový příklad, postupně pomocí vybraných technologií. Zadání tohoto příkladu bude součástí kapitoly věnující se způsobu řešení práce. Potřeby programátorů na perzistenci se projekt od projektu liší, často tak mohou být i protichůdné. Z tohoto důvodu nelze očekávat, že by bylo možné doporučit jednu univerzální technologii pro všechny. Součástí metody řešení práce tak musí být i určení projektů, kterých se budou doporučení týkat. Vzhledem ke stáří jazyka Java lze předpokládat, že možných technologií bude velké množství, pro zvládnutí práce a dodržení doporučeného rozsahu bude tedy nutné vybrat jen několik nejznámějších a nejrozšířenějších technologií. V následující části budou vybrané technologie prezentované s přihlédnutím ke sledovaným kritériím a na ukázkách řešení vzorového příkladu. Po prezentování technologií lze následné srovnání dle kritérií a uvedení jejich předností a nedostatků považovat za dostačující pro čtenáře, který si po přečtení pár stránek může zvolit vhodnou technologii pro svou potřebu a tu dostudovat podle zdrojů jinde. Dále čtenář prostudováním praktických ukázek získá možnost si jednotlivé technologie subjektivně porovnat podle množství potřebných úkonů k realizaci stejného řešeného příkladu. Mimo účelu srovnání může příklad u každé technologie sloužit i jako stručný návod, jak s touto konkrétní technologií pracovat.
1.2
Cíl práce
9
Na základě splnění předchozích úkolů a srovnání technologií bude možné naplnit i hlavní cíl této práce, kterým je doporučení vhodné technologie pro potřeby programátorů. Doporučení se bude nacházet v části s výsledky práce. Z důvodu již uvedené rozdílnosti projektů nelze ovšem očekávat jednoznačné doporučení jako „vždy používejte tuto technologiiÿ, ale doporučení různých technologií pro různé potřeby uvažovaných projektů.
2
ANALÝZA SOUČASNÝCH MOŽNOSTí
2
10
Analýza současných možností
Tato kapitola se zabývá analýzou problému této diplomové práce, konkrétně dostupnými možnostmi pro perzistenci dat v programovacím jazyce Java v době psaní této práce. Perzistence (od latinského persistens) se týká něčeho trvalého. V oblasti počítačů jde o trvalé uložení obsahu části operační paměti závislé na napájení na jiném nezávislém místě, kde údaje vydrží „věčnostÿ (nebo alespoň do příštího běhu programu) a budou kdykoliv k dispozici pro další použití. Na perzistenci se lze dívat z různých pohledů. Prvně například, jaké části běžícího programu mají být uloženy a proč. Jiné strategie se budou volit pro stav programu (např. aktuální krok a hodnoty proměnných u nějakého složitého výpočtu) a pro zpracovávané data (např. výplatnice zaměstnanců za poslední měsíc). Na to navazuje druhý pohled, čitelnost trvale ukládaných dat. Zatímco stav programu je většinou zajímavý jen pro samotný program a tak je ukládán v co možná nejúspornější a pro opětovné načtení nejefektivnější binární podobě specifické pro daný program (pro člověka je tato forma špatně čitelná), zpracovávané data můžou být zajímavé i pro jiné programy nebo obsluhující personál, tak jsou tyto data často ukládána v podobě snadno přenositelné do jiných systémů a pro člověka v čitelné podobě (třeba i s nutným zpracováním dat k prezentaci jiným programem, ale z podoby dodržující určité normy nebo standardy). Tím se úvaha o úhlech pohledu dostává k poslednímu, k místu uložení obsahu. To nezávisí na předchozích úhlech, protože ukládání stavů nebo dat neupřednostňuje žádné úložiště. Úložištěm můžou být různé souborové nebo jiné systémy, které uložené údaje transformují do očekávané podoby (např. relační databáze a jejich SQL). Možných úložišť je celá řada a liší se svými vlastnostmi. Pro analýzu možností v této práci jsou některé detailněji rozebrány v následující sekci.
2.1
Úložiště
Zde jsou stručně rozebrány možnosti ukládání do souborů, relačních a objektových databází. Všechny můžou ukládat textové i binární údaje. Soubory Soubory jsou ukládány na souborové systémy, přístup k nim může být řízen přístupovými právy. Nejčastěji jsou takto perzistovány stavy, reporty nebo exporty běžících programů. V případě dat se častěji jedná až o interně používané úložiště zvoleného systému (zde myšleno databáze) k perzistenci programem zpracovávaných dat. Pro malé množství dat (nepoužijí se databáze) nebo zmiňované reporty a exporty se často používají tzv. flat-file databáze (např. CSV), XML, tabulkové nebo jiné proprietární soubory.
2.1
Úložiště
11
Relační databáze Jde o nejrozšířenější místo pro ukládání perzistentních údajů, zejména zpracovávaných dat, některé aplikace zde však udržují i svůj stav. Jedná se o systémy, jejichž historie sahá do 70. let minulého století. Za duchovního otce je považován E. F. Codd, tyto databáze jsou totiž postavené na jeho relačním modelu. Podle této teorie data tvoří uspořádané n-tice hodnot, kde každá hodnota je prvkem atributu, ten má svůj význam a obor povolených hodnot. Množina všech možných hodnot n-tic stejných atributů je nazývána relací. Teorie dokazuje, že veškeré operace nad relacemi lze realizovat pomocí pěti základních operací, těmi jsou sjednocení, kartézský součin, rozdíl, selekce, projekce a spojení. Realizací relační teorie jsou relační databázové systémy (v této práci zkráceně označované jako RDBMS), kde relacím odpovídají tabulky, atributům sloupce a n-ticím hodnot řádky. Někdy je výrobci do databáze doplněna podpora pro objektové paradigma, takové systémy pak jsou označované jako objektově-relační databáze, protože interní uložení stále odpovídá relačnímu modelu. Objektové databáze Jedná se databázové systémy postavené na objektově orientovaném paradigmatu, v určitém smyslu je lze považovat za nástupce relačních databází a jejich používání ve světě vzrůstá. Objektové paradigma je postaveno na následujících konceptech. • Objekt je základním pojmem, označuje různé prvky reálného světa, které jsou v návrhu uvažovány a modelovány. Objekty mají své vlastnosti, chování a stav v určitém čase. • Třída je abstrakcí prvků reálného světa do modelu. Prvky se společnými vlastnostmi a chováním jsou modelovány jednou třídou. Pro model jsou vybrány jen směrodatné vlastnosti, ty tvoří atributy třídy. Chování prvků je ve třídách modelováno jejich metodami. • Instance je konkrétní podobou třídy s odpovídajícími hodnotami vybraných vlastností reálného prvku v určitém čase. • Posílání zpráv slouží pro komunikaci mezi instancemi (i různých tříd) a vyvolání požadovaného chování. Jedna instance posílá jiné zprávu s daty ke zpracování (parametry) a požadavkem na určité chování (provedení metody). • Zapouzdření znamená, že instance neposkytuje okolí možnost přístupu ke svým vlastnostem, pouze své chování pomocí definovaného rozhraní a komunikačního protokolu. • Dědičnost slouží k modelování společných vlastností a chování prvků zároveň se specializací, tedy možností nějakého specifického chování navíc oproti jiným prvkům. Společné části jsou modelovány jednou třídou (předkem), specializaci pak odpovídají další poděděné třídy (potomci). Společné části jsou tak modelovány na jednom místě bez problematické redundance. Příkladem může být
2.2
Známé optimalizace a problémy
12
třída plachetnice, jejíž instance umí plachtit. Instance specifické třídy motorová plachetnice dokáží navíc během bezvětří plout na motorový pohon. • Skládání umožňuje, aby atributem třídy byla i jiná třída. Modeluje se tak složení celku z částí, například uvedená motorová plachetnice se skládá ze stěžně, plachty, motoru atd. • Polymorfismus označuje specifické chování prvků na stejný podnět. Například třída zvíře sdružuje prvky, které jsou v reálném světě zvířetem a dělají nějaký zvuk (na třídě bude metoda udělej zvuk). Instance specializovaných tříd budou na příchozí zprávu s požadavkem o vyvolání této metody reagovat různě, instance třídy pes budou štěkat, instance třídy kočka zase mňoukat. Pro specifické případy použití jsou objektové databáze rychlejší než relační, neboť umožňují při vyhodnocování optimálnější navigaci mezi objekty na základě sledování ukazatelů. Odpůrci těchto databází považují takovou navigaci za moc specifickou a nepoužívání abstraktnějšího přístupu za krátkozraké. Někteří výrobci tak reagují poskytnutím i relačního přístupu k objektově uloženým datům.
2.2
Známé optimalizace a problémy
Zbytek této sekce se nejprve věnuje dvěma často používaným optimalizacím v implementaci software a jejich aplikaci při perzistenci, pak následuje rozbor problémů, kterým musí čelit programátoři využívající kombinaci objektových programovacích jazyků a relačních databází. Proxy objekty V případě složitých, dlouho trvajících výpočtů nebo paměťově objemných dat je často použit zástupný proxy objekt. Jedná se o návrhový vzor, více je možné se o něm dočíst v knize Design Patterns (Freeman, Sierra, Bates, 2004, kap. 11). Použitím tohoto vzoru je možné provedení náročného výpočtu nebo nahrání objemných dat odložit až na okamžik, kdy je jisté, že jsou výsledky nebo data potřebné. Při některých případech použití se totiž zpracování bez nich obejde. U perzistence jsou proxy využívány jako zástupné objekty, které data k referenci dočtou pomocí definovaných pravidel dodatečně až při prvním přístupu k metodám zastupovaného objektu. V práci je tato optimalizace nazývána jako tzv. lazy loading nebo česky opožděné nahrávání. Pamatování výsledků Složité výpočty není vhodné zbytečně opakovat, stejně tak opakovaně dotahovat často používané data. Pokud to tedy charakter výpočtu nebo dat umožňuje, výsledky se při stejném zadání rovnají nebo data nejsou na pozadí měněny nějakým jiným zpracováním, je možné si oboje po prvním získání někde zapamatovat a v budoucnu opakovaně použít.
2.2
Známé optimalizace a problémy
13
V případě perzistence k takovému dočasnému zapamatování dochází typicky na aplikačních serverech a eliminují se tak zbytečné volání business logiky nebo přístupy do databáze. V práci je tato optimalizace nazývána cachování, stručně použití cache, nebo českým opisem dočasné zapamatování. Impedance mismatch Relační a objektový přístup k návrhu a programování se liší, tak je při jejich společném použití většinou nutné řešit problémy často souhrnně označované jako tzv. impedance mismatch. Detailně je tento nesoulad vysvětlen například v knize Hibernate in Action (Bauer, King, 2004, kap. 1.2), stručně se jedná o problémy uvedené v následujících odstavcích. Problémem s granularitou se označuje skutečnost, kdy dvěma třídám ve vzájemném vztahu kompozice odpovídá pouze jedna tabulka. Například atributy tříd osoba a její adresa jsou v databázi společně uloženy v jedné tabulce osob. Relační databázové systémy nepodporují žádné vztahy mezi tabulkami, dědičnost tříd je tak dalším problémem, který vyžaduje řešení. V Javě se rozlišují dvě rovnosti, první je rovnost ukazatelů (jedná se o stejný objekt v paměti), druhou je rovnost na základě hodnot (řešena metodou equals()). U relačních databází je pouze jedna identita, tou je primární klíč. Vztahy se u objektů řeší referencí nebo kolekcemi, je možná různá kardinalita a rozlišuje se i směrovost. Oproti tomu v relačních databázích se vztahy evidují pomocí cizích klíčů, nejsou směrové a kardinalitu M : N je nutné řešit vazební tabulkou. Posledním problémem je navigace objektovým grafem. V případě objektům se k datům programátor dostává postupným sledováním referencí typicky přes různé třídy, kterým v databázi odpovídají různé tabulky. Každý přechod tak znamená další přístup a dotaz do databáze, což není efektivní. Pokud je navigace předem známá, je lepší data načíst hned prvním dotazem s využitím spojení. Optimistické zámky Při návrhu víceuživatelských aplikací je nutné brát v úvahu i transakce a jejich izolační úroveň, ta je podrobněji vysvětlena později v práci, u příkladu 4.1.2 na straně 28. Ve stručnosti lepší izolační úroveň zajistí, aby několik souběžných transakcí pracovalo s daty korektně, četlo správné hodnoty a nepřepisovalo navzájem data. Implementace takové úrovně je však náročnější, některé databázové systémy tak ani vyšší úrovně nepodporují. Vyšší úroveň zamyká data, tím snižuje možnou souběžnost paralelních transakcí a degraduje výkon aplikace. Z tohoto důvodu se často použije nižší úroveň, kdy je ovšem nutné souběžnou modifikaci stejného záznamu ve dvou transakcích řešit programově.
2.3
Dostupné technologie
14
Jsou možné dva přístupy, při prvním tzv. poslední vyhrává není nic řešeno a později zapsaná hodnota v databázi zůstane. Problémem je, že uživatel dřívější transakce si myslí, že jsou uložena jeho data. Druhý uživatel zase netuší, že přepsal data prvního uživatele. Častěji se tedy implementuje jiný přístup, označovaný jako poslední vyhrává, nebo také optimistické zamykání. Při tomto druhém přístupu úspěšně doběhne dřívější transakce a druhá pozdější končí chybou, která upozorní uživatele, že jím modifikovaná data se mezitím již změnila a je nutné úpravu udělat znovu. Za optimistický se tento přístup označuje z předpokladu, že k souběžné modifikaci stejných dat bude docházet minimálně, nemá tedy smysl kvůli zabránění takové modifikace používat vyšší izolační úroveň. Když už k souběžné modifikaci vzácně dojde, je detekována a nechává se na uživateli, aby ji ošetřil. Pro detekci se používá nově přidaný sloupec, který figuruje v modifikačních SQL příkazech spolu s primárním klíčem modifikovaného záznamu. Pokud se hodnota v tomto sloupci změní (vítězným konkurenčním přístupem), druhá modifikace nepostihne žádný záznam, to je detekováno a vyhozena patřičná chyba. Takto přidaný sloupec bývá typu časová známka nebo lépe celé číslo označující verzi záznamu. Detailněji je tato problematika vysvětlena například v knize Hibernate in Action (Bauer, King, 2004, kap. 5). N + 1 problém Tento problém souvisí s nevhodným použitím opožděného nahrávání. To má své místo tam, kde je potřeba z relační databáze dočíst pouze část dat, například při zobrazení přehledu faktur a následném vytištění detailu jedné z nich je zbytečné pro zobrazení přehledu načítat detaily všech faktur. Místo toho jsou detaily požadované faktury nahrány až pro tisk. Při jiném použití je ale předem známo, že se budou požadovat všechny data a jejich opožděné nahrávání zvyšuje počet přístupů k databázi a délku zpracování. Pokud je například v systému N faktur a je požadován export tisků detailů všech těchto faktur, přistupovalo by se při opožděném nahrávání do databáze jak pro seznam všech faktur, tak opakovaně pro detail každé z nich, celkový počet by tedy byl N + 1. Od používaných technologií se tak očekává, že v případě potřeby umožní i hromadné načtení dat najednou. V práci je taková podpora označována jako řešení N + 1 problému.
2.3
Dostupné technologie
Pro perzistenci (zejména stavu programu) je možné použít tzv. serializaci, která je součástí přímo Java Core API (Sun–SE6, 2008, Serializable). Jedná se o proces převodu stavu instance (hodnot atributů) na sekvenci bájtů a zpět.
2.3
Dostupné technologie
15
Instanci třídy dokáže virtuální stroj Javy (JVM) automaticky takto zpracovat, pokud třída implementuje prázdné rozhraní Serializable, zpracovány jsou všechny atributy, které nejsou označeny klíčovým slovem transient. Pokud je hodnotou atributu reference na instanci jiné třídy, musí tato třída také implementovat uvedené rozhraní. Pokud automatické zpracování nevyhovuje, má programátor možnost napsat vlastní, detaily uvádí dokumentace. Při hledání další vhodné technologie pro perzistenci zpracovávaných dat v Javě existuje několik možností, kam se podívat. Učebnice nebo seznamovací příklad (tzv. tutoriál ) určitě dovede i k základnímu kamenu pro databázovou perzistenci, kterým je JDBC. Se štěstím je možné se ve stejném zdroji dočíst i o dalších pokročilejších možnostech v podobě tzv. frameworků (v práci dále označovaných českým slovem rámec). Oblíbené a používané rámce pro perzistenci je možné dále hledat v článcích publikovaných na serverech věnovaných jazyku Java, případně v jejich diskuzích, fórech nebo blozích čtenářů. Nejlepším zdrojem jsou praktické zkušenosti ostatních, je-li se koho zeptat. Pro celkový přehled dostupných rámců pro perzistenci v Javě je vhodné navštívit webový server (Open Source in Java, 2009) věnující se této problematice. Prezentuje stručné informace o těchto rámcích: • Cayenne • Apache OpenJPA • Ibatis SQL Maps • Hibernate • OJB • Torque • Castor • TJDO • JDBM • Prevayler • JPOX Java Persistent Objects • Speedo • Jaxor • pBeans • SimpleORM • Smyle • XORM • Ammentos • Super CSV • ODAL • BeanKeeper • Persist • Ebean ORM • Space4J
2.3
• • • • • • • • • • • •
Dostupné technologie
16
JDBCPersistence O/R Broker JGrinder Velosurf jPersist Java Ultra-Lite Persistence PAT QLOR Daozero SeQuaLite Mr. Persister LightweightModelLayer Lze zde napočítat 36 rámců a to se přitom nejedná o kompletní seznam všech rámců, které lze pro perzistenci použít. Například takový TopLink na seznamu není, protože se nejedná o open source, přitom je na projektech také rozšířen, používá ho například společnost AIS Software, a.s. Rámec TopLink je produktem společnosti Oracle z řady Oracle Fusion Middleware, který jako implementace Java Persistence API slouží pro perzistenci dat. Mezi možné úložiště patří různé relační databáze, XML nebo historické nerelační zdroje. Na stránkách je přístupná přehledná prezentace o možnostech (Oracle, 2008, Quick Tour) tohoto produktu. Licence umožňuje vyzkoušení a vývoj bez poplatku, při konečném nasazení na provozní server se však už produkt platí. Poslední zde prezentovanou technologií je čistě objektová databáze Caché, které se v současnosti těší vzrůstající oblibě jako nejpoužívanější řešení v oblasti objektových databází (Bláha, 2008). Caché je produkt společnosti InterSystems s mnohaletou tradicí. Svou oblíbenost si získává díky více možným přístupům k uloženým datům, datovou strukturu je možné definovat buď pomocí objektů nebo jako relační tabulky, definici sama Caché automaticky převádí z jedné podoby do druhé. K datům lze přistupovat stejně, tedy jako k objektům přímo v programovacím jazyku, nebo jako k relačním tabulkám přes SQL a JDBC. Navíc je možný i vícerozměrný přístup k datovým polím, které Caché interně k ukládání používá. Její nasazení na projektu je limitováno licencí. Další informace o této databázi a zkušební verzi lze najít na domovských stránkách (InterSystems, 2007).
3
METODY ŘEŠENí
3
17
Metody řešení
Tato kapitola se zabývá způsobem řešení této diplomové práce, jejímž cílem je podpora při rozhodování vhodného způsobu řešení perzistence dat. Nejprve identifikuje kritéria, na základě kterých je možné technologie pro perzistenci srovnat. V následující sekci uvádí zadaní vzorového příkladu, jehož implementací je možné ukázat rozdílnosti zkoumaných přístupů k prezistenci. V předposlední části rozděluje možné softwarové projekty do kategorií, kterým je ve výsledku práce možné doporučit konkrétní technologii. Kapitola končí vybráním technologií k dalšímu zkoumání.
3.1
Srovnávací kritéria
Pro doporučení vhodné technologie je potřeba je prozkoumat z více stran, v této práci jsou sledována kritéria uvedená na tomto místě. V této sekci jsou jednotlivá kritéria zvýrazněna. Související kritéria jsou vždy spojena do jedné skupiny. Názvy a pořadí těchto skupin je jednotné v celé práci, tedy zde i v další kapitole, kde je každá sekce věnovaná zvlášť konkrétní technologii. Zázemí Takto pojmenovaná část sleduje kritéria kolem zázemí zkoumané technologie. Například jaká je její historie a stáří, jakou licencí je omezeno její použití, jak je široká komunita vývojářů nebo kvalitní a přístupná dokumentace. Otevřenost V rámci otevřenosti je zkoumána možnost přechodu na jinou databázi během vývoje nebo provozování aplikace. Jsou tím myšleni dodavatele databázového stroje, zajímaví jsou tedy podporovaní výrobci a složitost jejich záměny. Vedle změny výrobce databáze se práce zajímá i o technologií podporované jazyky. Cílem není nahradit Javu jiným jazykem, ale u rozsáhlejšího projektu se může stát, že různé části aplikace jsou napsané v různých jazycích, přičemž databáze je společná. V takovém případě je výhodou, pokud ve všech částech může tým pro přístup do databáze používat stejné postupy. Výkon Pro srovnání technologií je součástí práce pokus o změření jejich výkonu a zaměření se na možnosti, jak technologie umožňují přístup k databázi optimalizovat. Například použitím opožděného nahrávání až při použití odpovídající get metody, nebo cachováním jednotlivých instancí nebo výsledků celých dotazů, nebo dávkovým zpracováním.
3.2
Vzorový příklad
18
Podpora objektových principů Tato část se zajímá, jakou podporu mají technologie pro charakteristické principy objektů, jakými jsou vztahy mezi objekty, dědičnost a polymorfní dotazy. Složitost V této části se pohled zaměřuje na složitost technologie, jakou má učící křivku, jaké jsou potřebné znalosti před a během jejího používání, jaká je velikost knihoven (jak počet, tak skutečná velikost nutných a doplňkových knihoven). Použitelnost Závěr z pohledu autora subjektivně hodnotí pocity z použití, jaké byli nalezeny výhody a nevýhody. Tato skupina kritérií je pro přehlednost uvedena až po představení všech srovnávaných technologií. Rekapitulace Pro přehlednost je zde zopakován výčet dílčích kritérií, které jsou nadále zkoumány: • historie, • licence, • kvalita dokumentace, • podporované databáze a přechod mezi výrobci, • podporované programovací jazyky, • výkon, • opožděné nahrávání, • cachovaní, • dávkové zpracování, • vztahy, • dědičnost, • polymorfní dotazy, • učící křivka, • potřebné znalosti, • velikost knihoven, • pocity, • výhody a nevýhody.
3.2
Vzorový příklad
Zkoumané technologie budou srovnány na základě jednoduchého příkladu na části datového modelu z prostředí vysoké školy. Perzistentní vrstvu podle terminologie MVC řeší pouze model, pro účely této práce je tak zbytečné psát kód řešící zbývající
3.2
Vzorový příklad
19
pohled a kontrolér. Použití perzistentní vrstvy lze dostatečně ukázat pomocí testů s využitím rámce jUnit. Doména příkladu je omezená na osoby obecně, jejich specializaci v podobě studentů a dále studijní program studentů. Detailněji doménu ilustruje následující třídní diagram. Místo tříd jsou použity rozhraní, to umožňuje pomocí jednou napsaného testu ověřit postupně implementace ve všech zkoumaných technologiích.
Obr. 1: Třídní diagram domény
Na osobách a studentech lze ukázat řešení dědičnosti a podporu polymorfních dotazů. Studijní program a studenti ilustrují vztah 1 : N . Následující diagram ilustruje strukturu testů.
Obr. 2: Třídní diagram testů
Základní funkcionalitu ověřuje test FunctionalTestCase, přehled jeho metod a stručně komentovaný účel ukazuje následující výčet, kompletní zdrojový kód testu lze najít v přílohách práce.
3.2
• • • • •
Vzorový příklad
20
testSelect – Testuje získávaní dat podle hodnoty klíče. testInsert – Testuje zakládání dat. testUpdate – Testuje aktualizaci dat novými hodnotami. testDelete – Testuje mazaní dat podle hodnoty klíče. testOptimisticUpdate – Testuje detekci konkurenční modifikace na základě optimistického zamykání. • testLazyLoadingAndInstanceCaching – Testuje funkčnost opožděného nahrávání a optimalizaci pomocí cache. • testDynamicQuery – Testuje vyhledávání osob na základě zadání různé kombinace příjmení a jména. • testPolymorphism – Testuje rozlišení třídy ve vráceném výsledku dotazu. • testSameInstance – Testuje, zda je různými dotazy vrácena stejná instance jedné entity. Ověření výkonu je v PerformanceTestCase, následuje výčet metod, kompletní zdrojový kód je opět součástí příloh. • testFullReports – Zjišťuje dobu potřebnou na vygenerování reportu s řešením N + 1 problému. • testLazyReports – Zjišťuje dobu potřebnou na vygenerování stejného reportu s použitím opožděného nahrávání. Oba scénáře ověřují funkčnost přes rozhraní TestCaseUtil a SchoolUtil, následující výčet stručně popisuje jaký účel jejich metody mají. Rozhraní TestCaseUtil: • void create(); – Volána jednou ze statického bloku obou testovacích scénářů, úkolem je inicializace technologie a příprava všeho potřebného. • void prepareData(); – Volána před každým funkčním testem, může nachystat databázovou strukturu a musí dávkově vložit testovací data. • void cleanData(); – Volána před a po každém funkčním testu, musí vyčistit testovací data. • void callPrepare(); – Volána před každým výkonovým testem, zavoláním PL/SQL procedury z příloh práce připravuje testovací data. • void callClean(); – Volána před a po každém výkonovém testu, zavoláním PL/SQL procedury z příloh práce vyčistí testovací data. • void getSchoolUtil(); – Pomocná metoda na získání implementace v testované technologii. Rozhraní SchoolUtil: • Person newPerson(); – Pomocná metoda na získání prázdné instance osoby v testované technologii. • Student newStudent(); – Pomocná metoda na získání prázdné instance studenta v testované technologii. • Person findPerson(long id); – Vrací osobu nebo studenta podle zadaného id, prezentuje získávání dat včetně polymorfního dotazu.
3.3
Rozdělení projektů
21
• List findPersons(String firstName, String surname); – Vrací list osob nebo studentů podle zadaných parametrů, prezentuje získávání dat pomocí dynamického dotazu podle vyplnění částí jména. • List getAllStudents(); – Vrací list všech studentů. • long storePerson(Person person); – Uloží novou nebo aktualizuje existující osobu (případně studenta) a vrací její id. Při nové osobě ukazuje získání vygenerovaného klíče. Při úpravě ilustruje použití optimistických zámků. • long removePerson(long id); – Vymaže osobu nebo studenta podle zadaného id. • String generateTermLazyReport(); – Metoda vrací report studentů a jejich studijních programů. Programy jsou načítány dodatečně (ukázka lazy loading). • String generateTermReport(); – Metoda vrací stejný report studentů a jejich studijních programů. Programy jsou načítány zároveň se studenty (ukázka řešení N + 1 problému). • Program findProgram(long id); – Vrací studijní program podle zadaného id.
3.3
Rozdělení projektů
Potřeby programátorů na perzistenci se projekt od projektu liší, často tak mohou být i protichůdné. Z tohoto důvodu nelze očekávat, že by bylo možné doporučit jednu univerzální technologii pro všechny. Má-li tato práce přesto nějakou technologii doporučit, musí se napřed určit kategorie projektů podle jejich vlastností. S takovým rozdělením již bude možné doporučit určitou technologii pro danou kategorii. Pro doporučení vhodné technologie jsou v této práci uvažovány následující kategorie projektů. • Projekt s omezením na verzi Javy – zejména staré projekty jsou realizované na předchozích verzích Javy a přechod na novější je nemyslitelný (velké riziko, obrovské náklady). • Projekt s existujícím modelem – v případě projektů, které navazují nebo se integrují s existující aplikací bývá těžké nebo nemožné změnit dříve navržený model uložení dat (dopad na velké množství aplikací). • Projekt s vlastním návrhem – jde o opak předchozího projektu, kdy se začíná nově a model uložení dat je možné navrhnout podle vlastních potřeb. • Projekt se sdílenou databází – jedná se o projekty, kde jsou data v databázi měněna i z jiných aplikací, které pod projekt nepatří (cachování v těchto případech nemusí být přínos). • Projekt s exkluzivní databází – jde o opak předchozího projektu, kde projektová aplikace je jediná, která data v databázi mění. • Malý projekt – projekt, který minimálně využívá databázi (nejde zde o rozsah projektu, ale o rozsah perzistentních úkolů). • Projekt kritický na výkon – projekt, u kterého je kriticky důležitý čas, jak dlouho perzistentní operace trvají.
3.4
Vybrané technologie
22
• Projekt kritický na dobu realizace – projekt, u kterého je důležité za jak dlouho bude nasazen (faktor často pojmenovaný jako time to market).
3.4
Vybrané technologie
Dřívější kapitola uvádí celé spektrum možností, jak perzistenci v Javě řešit. Pro rozumný rozsah práce a pro dostatečnou hloubku zkoumání určité technologie je nutné zvolit kompromis a vybrat jen část možností pro další zkoumání. Nejrozšířenějším místem pro ukládání perzistentních dat jsou pravděpodobně relační databáze, zbytek práce je tedy věnován řešení perzistence s jejich využitím, tedy ukládání dat z objektového jazyka Java do obecné relační databáze. Dostupných technologií pro společné použití Javy a relační databáze, prezentovaných i v dřívější kapitole, je stále moc velké množství na rozumné zvládnutí v jedné práci. Další zkoumání je tak omezeno na technologie s volným použitím a prokazatelným využíváním na některém blízkém projektu (s nadějí, že projekty zvolily vhodné řešení). JDBC JDBC je nutným základem, který se musí při řešení perzistence zmínit. Jedná se přímo o API jazyka, po detailním představení teprve patřičně čtenář ocení přínos nadstavbových rámců (ty totiž interně JDBC používat musí). Tím je vyjímkou z podmínky využívání na projektu, protože JDBC v čisté podobě není moc rozšířené. iBATIS Jedná se o oblíbený mezistupeň mezi JDBC a plnohodnotnými ORM rámci. Pro řadu problémů lze doporučit jako nejvhodnější řešení. Dle zkušeností autora této práce část svých aplikací na této technologii realizuje např. Home Credit Internacional, a.s. nebo Tatra banka. Hibernate Asi nejznámější a hodně používaný ORM rámec, práci dokáže značně ulehčit, ale na druhou stranu při špatném použití zavleče množství nových problémů. Některé své zakázkové aplikace realizovala na této technologii například i akciová společnost Unicorn.
4
POROVNÁNí TECHNOLOGIí
4
23
Porovnání technologií
Tato kapitola je členěna do sekcí podle vybraných technologií. Každá sekce má podsekce podle vybraných kritérií na srovnání popsaných v sekci 3.1 na straně 17. Poslední podsekcí u každé této sekce jsou některé zajímavé aspekty řešení vzorového příkladu v dané technologii. Zadání celého řešeného příkladu je v sekci 3.2 na straně 18. Obsahem následující sekce je subjektivní pohled programátora na zkoumané technologie a uvedení zjištěných kladů a záporů. Toto ohodnocení není pro přehlednost součástí textů věnovaných jednotlivým technologiím, ale je uvedeno až po představení všech zkoumaných technologií na jednom místě. Předposlední sekcí je vyhodnocení srovnání technologií na základě vytčených kritérií. Kapitola končí sekcí s doporučením konkrétní ze zkoumaných technologií pro určitou kategorii projektu podle dělení uvedeného dříve v práci v části 3.3 na straně 21.
4.1
JDBC
JDBC (Java Database Connectivity) je základním API v Javě pro práci s relační databází, lze ho úspěšně použít i pro zpracování dat v excelu nebo „flat fileÿ databázích. Jedná se pěkný příklad použití návrhového vzoru DAO (Data Access Object), jak prezentuje kniha iBATIS in Action (Begin, Meadors, Goodin, 2004, kap. 10.1).
Obr. 3: JDBC jako vzor DAO
Součástí Javy jsou jen definované jednotné rozhraní pro práci a přístup k datům, konkrétní implementace v podobě driveru je pak zodpovědností jednotlivých výrobců databázových strojů (DBMS). Jedná se o základní kámen při práci s databází a ostatní technologie srovnávané v této práci ho musí využívat, detailněji se tomuto tématu věnuje každá sekce věnované srovnávané technologii.
4.1
JDBC
24
Zázemí JDBC je součástí Java Core API už od verze 1.1, od té doby bylo několikrát aktualizované. V době psaní této práce je dostupná Java 6, která nově obsahuje i JDBC verze 4.0. Poslední verze přináší vylepšení jako: • automatické nahrávání JDBC driveru, • Connection management, • RowId, • SQL dotazy jako anotace metod, • vylepšení práce s výjimkami, • a změny v API (včetně nových datových typů). Stručné vysvětlení těchto novinek je na blogu (Pichlík, 2006a), detailnější vysvětlení lze najít v článcích (Penchikala, 2006) nebo (Acharya, 2007). Jak už bylo naznačeno v úvodu a je rozebráno i u dalších technologií, JDBC je nejpoužívanějším způsobem, jak přistupovat k datům. Jeho použití ale není triviální, je náchylné k chybám a velká část kódu se neustále opakuje1 . Z uvedených důvodů se čistě JDBC na projektech většinou nepoužívá. Duplicitní a rizikový kód je zkušenými programátory odsunut do pomocných tříd (Pichlík, 2006b) a pro ostatní členy týmu tak vzikají základy interních rámců. Některé z nich nabydou větších rozměrů a někdy můžou i opustit prostředí jedné společnosti, tak se z nich stane veřejný rámec pro perzistenci (například iBATIS). JDBC je tedy nejrozšířenější technologií, ale programátor ho používá až skrze další vrstvu. Díky tomuto masivnímu rozšíření není problém hledat řešení běžných problémů. JDBC je tvořeno rozhránímy ze dvou balíků java.sql a javax.sql, ke kterým je na webu dostupná tradiční JavaDoc dokumentace2 (Sun–SE6, 2008). Pro detailnější nastudování existuje řada rozsáhlých knih, například JDBCTM API Tutorial and Reference (Fisher, Ellis, Bruce, 2003), která je zatím dostupná jen v 3. vydání, tedy jako specifikace pro verzi 3.0. Otevřenost Přenositelnost vyvinuté aplikace na databázový stroj jiného výrobce by díky standardu SQL jazyka a jednotnému API v jazyce Java měla být teoreticky snadná. Praxe bohužel ukazuje, že je tomu ve většině případů právě naopak. Při přechodu na jinou databázi se musí začít používat jiný driver, který ale nemusí implementovat všechny funkčnosti, které implementoval dříve používaný driver. Přechod tak zvyšuje pracnost o testování nového driveru a přepisování nepodporované funkčnosti (příkladem může být získávání hodnoty automaticky generovaného klíče při vkládání nového záznamu). I když není měněn výrobce databázového serveru, ale jen aktualizován driver, nejsou tyto zvýšené náklady ušetřeny. 1
Jde o získávání Connection a její správné uvolňování či zavírání. Právě takový kód je typicky odsouván do rámců. 2 Jedná se o API Specification k Java SE 6, tedy dokumentaci k JDBC 4.0. Dokumentace k předchozím verzím je ve stejné formě také dostupná.
4.1
JDBC
25
Ještě častějším problémem jsou různé SQL dialekty jednotlivých výrobců, kteří používají vlastní pojmenování pro datové typy nebo se liší způsoby generování primárních klíčů (autoincrement, sekvence atd., více je tomuto tématu věnováno v sekci o Hibernate). Dopady přechodu na jinou databázi v tomto případě závisí na kvalitě přenášeného zdrojového kódu. V optimálním případě jsou všechny SQL řetězce bez duplicit a uceleně na souvislém místě (jedna třída nebo projekt), nutné přepisování je tedy pro každý problém provedeno jen jednou a místo, kde má být provedeno není nutné složitě hledat. V opačném případě (SQL řetězce jsou nedetermisticky rozmístěny v celém kódu) je nutné provést kompletní revizi a stejné úpravy je nutné opakovaně provádět na všech výskytech jednoho SQL elementu (myšleno například jako insert do jedné konkrétní tabulky). Dále je ještě nutné zmínit použití speciálních funkčností konkrétní databáze (například „stromečkový selectÿ na Oracle), v takovém případě je nutné před přechodem počítat s přepisem, protože je používán nepřenositelný kód a v těchto případech nikdy žádná technologie nepomůže. Myšlenka používání rozhraní, které zakrývá konkrétní implementaci, patří mezi obecné praktiky pro dobrý návrh a vývoj aplikací. JDBC je uvedení této myšlenky do praxe pro programovací jazyk Java a přístup k datům. V případě dalších programovacích jazyků je filosofie podobná, JDBC je tak možné přirovnat k ODBC. U JDBC existují 4 různé typy ovladačů, kde typ 1 interně využívá právě ODBC. Podrobněji jsou rozdíly mezi jednotlivými typy vysvětleny téměř v každém tutoriálu (Šeda, 2003). V serverovém prostředí je pro dosažení snadné přenositelnosti vhodné používat DataSource3 , který je na serveru dohledán přes své JNDI jméno. Takto získaná instance následně slouží jako továrna pro získávání Connection. Vytvoření nového spojení na databázi je náročné na prostředky a čas, z tohoto důvodu továrna místo vytváření nového spojení pro každý požadavek udržuje „poolÿ volných předpřipravených a opakovaně použitelných spojení. Dochází tak k urychlení běhu databázových aplikací. Zároveň taková továrna usnadňuje přechod na databázový stroj jiného výrobce, protože změna v řetězci definujícím připojení, který je pro každého výrobce a driver specifický, je provedena jen v konfiguraci serveru, bez zásahu do zdrojového kódu. Výkon Z pohledu připojení k relační databázi je JDBC nejrychlejší technologie, kterou je v současnosti v Javě možné použít, pokud se neuvažuje vyhledání a používání nástroje s nativní komunikací na používaný databázový server. Ostatní technologie (rámce) jsou nadstavbou JDBC a jejich použití ulehčuje a urychluje vývoj, přidává však další vrstvy, které spotřebují část výkonu na svou režii. 3
Místo statické metody java.sql.DriverManager.getConnection(...) je používána metoda javax.sql.DataSource.getConnection(...). Celkově rozhraní a události v balíku javax.sql jsou určené pro serverové prostředí jako základní součást Java EE.
4.1
JDBC
26
Samotný čas potřebný na provedení SQL příkazu a zpracování případného výsledku není jediný faktor, který ovlivňuje celkový výkon aplikace. Mezi další faktory patří neprovádění zbytečných dotazů, tedy odložení zpracování na dobu kdy je jisté, že jsou data požadována (tzv. „lazy loadingÿ), případně provedení dotazu pouze jednou a zapamatování si výsledků pro další použití, kde to charakter dat a hlavně četnost jejich změny dovoluje (tzv. „cachingÿ). U těchto faktorů JDBC žádnou podporu neposkytuje a programátor musí v případě potřeby takové chování aplikace řešit vlastním kódem. Faktory uvedené v předchozím odstavci optimalizují dotazovací DML příkaz, ostatní DML příkazy lze optimalizovat pomocí dávkového zpracování, kdy se příkazy shlukují a posílají na zpracování hromadně, čímž se optimalizuje např. síťová komunikace. JDBC má pro tyto dávky přímo podporu v API, použití ilustruje příklad 4.1.3. Stejně jako poslední zde uvedenou optimalizaci, kterou je PreparedStatement. Jedná se o předpřipravený DML příkaz určený k opakovanému použití, který navíc zabraňuje v použití „SQL injectionÿ útoku. Optimalizace spočívá v analyzování příkazu pouze jednou, nikoliv při každém použití. Podpora objektových principů JDBC je rozhraní pro prácí s databází, které nemá podporu objektových principů jako vztahy mezi objekty, dědičnost tříd a související polymorfní dotazy. Vyřešení těchto úkolů je tedy na programátorovi. Pro vztahy typicky vznikají funkce, které pomocí dotazů získají data pro související objekty a vrací jejich instance. Pro zadání příkladu této prace to tak muže být metoda, která na základě klíče studijního programu dohledá všechny studenty tohoto programu a vrátí je jako list. Na programátoru pak je, aby na vhodném místě tuto metodu volal a vrácený list přiřadil jako hodnotu instanční proměnné v třídě studijních programů. Dědičnost a její mapování je detailně řešeno v části o Hibernate. Tam popsané mapování lze realizovat i v JDBC, musí se ovšem celé ručně napsat. Pokud jsou například data studenta ukládána do více tabulek (osoba a student), je nutné všechny operace dělat dvakrát nad oběma tabulkami, v případě dotazů pak obě tabulky spojovat. Příklady 4.1.3 na straně 29, 4.1.4 na straně 29 a 4.1.5 na straně 32 ukazují části takového řešení. Polymorfní dotaz souvisí s dědičností, u navrácených objektů předem není známé, instancí jaké budou třídy. Příklad 4.1.4 na straně 29 ukazuje, že pro zadaný klíč je vrácena instance osoby nebo studenta v závislosti na existenci záznamu v tabulce studentů. Obdobně od jiné metody je možné očekávat, že na zadané kritéria vrátí list, ve kterém budou jak osoby, tak studenti, kteří takové kritéria splňují. Složitost Základy JDBC se lze naučit poměrně snadno, v závislosti na zkušenostech a znalostech se dají dostupné tutoriály absolvovat v řádu minut až hodin. V tutoriálech se občas vyskytují chyby, nebo jsou některé konstrukce nedostatečně vysvětlené,
4.1
27
JDBC
tím se zvyšuje riziko špatného použití v produkčním kódu. Z tohoto důvodu pořádné ovládnutí JDBC a dostudování všech možností a detailů je následně v řádu týdnů až let (v závislosti na studijních materiálech a časových možnostech programátora4 ). Pro zvládnutí JDBC technologie se předpokládají znalosti jazyka Java, DML a DDL příkazy jazyka SQL, povědomí o transakčním zpracování a případně zkušenosti s uloženými procedurami na databázových serverech. JDBC je součástí Java Core, pro použití je nutné ale v parametru classpath doplnit cestu ke knihovně s driverem. Knihovna může být specifická pro typ JDBC, a v případě typu 4 je specifická i pro konkrétního dodavatele databázového stroje. Pro srovnání v této práci tak nejde jednoznačně určit velikost potřebných knihoven. Orientačně je uvedena alespoň velikost knihoven použitých pro testy u této diplomové práce, včetně jména driveru. Tab. 1: Velikost knihoven JDBC
Vzorový příklad V této části jsou komentované ukázky kódu, které řeší některé partie příkladu zadaného v sekci 3.2 pomocí technologie JDBC. Pro použití JDBC je nejprve nutné nahrát do JVM driver pro konkrétní DBMS pomocí reflexe, ukázkou je implementace metody create z TestCaseUtilJDBCImpl. Příklad 4.1.1 (Nahrání JDBC driveru) public void create() { try { // volání newInstance() je nutné pro některé JVM // forName(String className) Class.forName(driverClass).newInstance(); } catch (Exception e) { log.error("Error while loading JDBC driver.", e); } } Od verze Java 6 není nutné toto volání provádět, pokud je driver definován pomocí manifestu. Pro kompatibilitu se starší verzí je kód uveden. V ukázce úmyslně není jméno třídy jako řetězec ale jako proměnná typu řetězec, jejíž hodnota je závislá na konfiguraci. Je tak dosažena možnost změny driveru bez nutnosti rekompilace 4
Pro delší čas se předpokládá dohledávání potřebných informací až při potřebě řešit konkrétní problém, nikoliv absolvování nějakého kurzu zaměřeného na JDBC, zároveň větší pracovní vytížení při řešení úkolů z jiných oblastí a tak nedostatek času na čtení specifikace o více jak tisíci stránkách.
4.1
JDBC
28
zdrojových kódů. Jméno driveru je nutné vždy dohledat v dokumentaci konkrétní DBMS. Všechna práce s databází je skrze spojení (Connection), jeho získání ilustruje další ukázka. Příklad 4.1.2 (Získání JDBC spojení) // potřebné importy import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public synchronized Connection getConnection() { if(c==null) { try { c = DriverManager.getConnection(url, user, password); c.setAutoCommit(false); c.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); } catch (SQLException e) { log.error("Error while getting Connection.", e); } } return c; } Podobně jako v předchozím případě, i zde je bráno URL, uživatelské jméno a heslo z konfigurace. Formát URL je opět specifická záležitost konkrétního DBMS a je nutné ho ověřit v dokumentaci. U spojení lze nastavit a měnit jeho vlastnosti. V ukázce se nastavuje, aby se neprovádělo automatické potvrzení transakce po každém příkazu a maximální možná míra izolace transakcí. Následující výčet stručně popisuje, jaké jsou možné úrovně izolace transakcí, DBMS nemusí podporovat všechny možnosti5 . • Connection.TRANSACTION_NONE – Neexistují vůbec transakce. • Connection.TRANSACTION_READ_UNCOMMITTED – V transakci jsou okamžitě viditelné změny v datech z jiných transakcí, nezávisle na jejich potvrzení nebo vrácení. • Connection.TRANSACTION_READ_COMMITTED – V transakci jsou viditelné změny v datech z jiných transakcí až po jejich potvrzení. Řeší tzv. čtení špinavých dat. • Connection.TRANSACTION_REPEATABLE_READ – V transakci jsou opakovaně čtená data stejná, i když je jiná transakce mezitím změnila s potvrzením. Řeší tzv. opakovatelné čtení. • Connection.TRANSACTION_SERIALIZABLE – Maximální míra, transakce dávají takový výsledek, jakoby byly prováděny sériově. Řeší tzv. fantómy. 5
Například v HSQLDB jsou transakce podporovány, ale nejsou vůbec izolované (úroveň READ UNCOMMITTED), naopak Oracle podporuje pomocí snímků i maximální úroveň.
4.1
JDBC
29
Přes otevřené spojení je možné provádět všechny SQL operace, třeba dávkově přichystat testovací data. Příklad 4.1.3 (JDBC dávka přes Statement) // potřebné importy import java.sql.Connection; import java.sql.Statement; private void batchDataInsert(Connection c) throws SQLException { Statement s = c.createStatement(); s.addBatch("insert into person(firstname, surname) " + "values (’Jan’,’Novák’);"); s.addBatch("insert into student(id, id_program) " + "values (identity(),1);"); s.addBatch("insert into person(firstname, surname) " + "values (’Marek’,’Starý’);"); s.addBatch("insert into student(id, id_program) " + "values (identity(),1);"); s.addBatch("insert into person(firstname, surname) " + "values (’Marek’,’Novák’);"); // poslední je jen osobou, nikoliv studentem s.executeBatch(); s.close(); } SQL příkazy se do dávky přidávají metodou addBatch(String sql) na Statement. Dávka je provedena celá až v metodě executeBatch(). Programátoři nesmí zapomínat uzavírat Statement hned, kdy už není potřeba, slouží k tomu metoda close(). Dávky lze řešit více optimálním způsobem přes PreparedStatement, je tomu dále věnována poznámka u příkladu 4.1.5 na straně 32. Z ukázky je patrné, že v JDBC se musí dědičnost řešit vlastními silami, při zakládání nových studentů je tedy potřeba vkládat zvlášť data jak do tabulky osob, tak do tabulky studentů. V terminologii Java Persistence API (JPA) se tato strategie ukládání dědičnosti v relačních databázích označuje jako InheritanceType.JOINED. Ostatní strategie a jejich detailnější popis je uveden až v sekci o Hibernate, kde je pro dědičnost lepší podpora. Následující příklad ukazuje, jakým způsobem uložené data načítat, jedná se o dohledání osoby nebo studenta na základě identifikátoru. Příklad 4.1.4 (JDBC selekt přes PreparedStatemnet) public static final String TABLE_STUDENT = "student"; public static final String TABLE_PERSON = "person"; public static final String SQL_FIND_PERSON_POLYMORPHIC; static { // příprava dotazu s použitím konstant na zpracování StringBuilder sb = new StringBuilder();
4.1
JDBC
30
sb.append("select\n"); sb.append("\tp.");sb.append(PersonImpl.ID);sb.append(",\n"); sb.append("\tp.");sb.append(PersonImpl.FIRSTNAME);sb.append(",\n"); sb.append("\tp.");sb.append(PersonImpl.SURNAME);sb.append(",\n"); sb.append("\tp.");sb.append(PersonImpl.VERSION);sb.append(",\n"); sb.append("\ts.");sb.append(StudentImpl.PROGRAM_ID);sb.append("\n"); sb.append("from "); sb.append(TABLE_PERSON);sb.append(" p left join "); sb.append(TABLE_STUDENT);sb.append(" s\non "); sb.append("p.");sb.append(PersonImpl.ID); sb.append("=s.");sb.append(StudentImpl.ID);sb.append("\n"); sb.append("where ");sb.append(PersonImpl.ID);sb.append("=?\n"); SQL_FIND_PERSON_POLYMORPHIC = sb.toString(); } public Person findPerson(long id) { log.debug(SQL_FIND_PERSON_POLYMORPHIC); // singleton s otevřeným spojením Connection c = TestCaseUtilJDBCImpl.getInstance().getConnection(); PreparedStatement s = null; Person result = null; try { s = c.prepareStatement(SQL_FIND_PERSON_POLYMORPHIC); s.setLong(1, Long.valueOf(id)); ResultSet r = s.executeQuery(); if(r.next()) { // nutné zjistit, instanci jaké třídy vytvořit if(r.getObject(StudentImpl.PROGRAM_ID)==null) result = new PersonImpl(r); else result = new StudentImpl(r); } } catch (SQLException e) { log.error("Error while finding person by id.", e); } finally { if(s!=null) try { // pres statement se zaviraji i jeho resulty s.close(); } catch (SQLException e) { log.error("Error while closing statement for finding " + "person by id.", e);
4.1
JDBC
31
} } return result; } V této ukázce je jasně vidět problém s čitelností SQL příkazů v JDBC. Složité SQL je nereálné psát jako jeden dlouhý řetězec, je nutné tedy použít skládání řetězců přes StringBuffer nebo StringBuilder. Čitelnost zhoršuje i použití konstant pro jména sloupců a tabulek v databázi. V tomto příkladu jsou konstanty i přesto použité, protože jejich přínosem je definice na jediném místě, je tak snadné změnit jméno tabulky, pokud se návrh modelu změní. Pro srovnání při přejmenování tabulky person v předešlém příkladu 4.1.3 je nutné upravit všechny tři příkazy pro vložení. Pro další příklady na JDBC v této práci je však dána přednost čitelnosti a konstanty při tvorbě SQL příkazů už nejsou použité. Nejprve je poskládán dotaz, který dotáhne data z tabulky osob a přidá i data pro studenta, pokud existují (left join). Na místě, kam má přijít identifikátor hledané osoby, je použit zástupný znak ?. Následně je takto sestavený dotaz použit při získání PreparedStatement z otevřeného spojení. Takto získaný objekt je připraven na opakované použití pro různé identifikátory. Před použitím musí být vždy všem výskytům ? přiřazeny hodnoty pro volání (tzv. bindování parametrů), stane se tak použitím metod setXXX(int index, XXX hodnota), kde XXX je nutné doplnit podle typu hodnoty, index určuje kolikátému výskytu ? má být přiřazena hodnota a je číslován od 1. Dotazovací příkazy (select) jsou provedeny metodou executeQuery(), která vrací ResultSet, který má velké množství možností. Typické použití je v příkladu, nejdříve je metodou next() posunut ukazatel na první nalezený záznam. Tato metoda vedle přesunutí ukazatele vrací i pravdivostní hodnotu, zda následující záznam existuje. V tomto případě je očekáván nejvíce jeden záznam, v případě očekávání více záznamů je tato metoda typicky volána v podmínce cyklu. Pokud jsou nějaké řádky nalezeny, hodnoty jednotlivých sloupců lze získat metodami getXXX(int index) nebo getXXX(String jménoSloupce), kde je opět nutné XXX doplnit v závislosti na očekávaném typu hodnoty. V tomto případě existence neprázdného identifikátoru studijního programu určuje, že vyhledaná osoba je i studentem. Ve volaných konstruktorech jsou pak pomocí metod stejného typu vyhledané hodnoty nastavené do atributů vytvářené instance. I když v tomto případě je PreparedStatement použit jen jednou, je jeho použití vhodnější než Statement, protože zamezuje SQL injection útoku6 a je možné i rychlejší provedení příkazu7 . 6
Ten je možný, pokud jsou v tomto případě identifikátor, obecně jakékoliv vyhledávací kritéria zadávány uživatelem. 7 V případě, že je stejný příkaz používán v aplikaci častěji a DBMS si již analyzované příkazy dočasně udržuje.
4.1
JDBC
32
Poslední příklad ukazuje, jakým způsobem lze volat ostatní DML operace (insert, update, delete) a DDL operace. Příklad 4.1.5 (JDBC insert se zjištěním klíče) private Long insertPerson(Person person) { String sql = "insert into person(firstname,surname) values (?,?)"; log.debug(sql); Connection c = TestCaseUtilJDBCImpl.getInstance().getConnection(); PreparedStatement s = null; CallableStatement cs = null; ResultSet key = null; try { s = c.prepareStatement(sql); s.setString(1, person.getFirstName()); s.setString(2, person.getSurname()); int r = s.executeUpdate(); if(r==1) { // něco vloženo, získat poslední id sql = "call identity()"; log.debug(sql); cs = c.prepareCall(sql); key = cs.executeQuery(); key.next(); long personId = key.getLong(1); if(person instanceof Student) { s.close(); s = c.prepareStatement( "insert into student(id, id_program) values (?,?)"); s.setLong(1, Long.valueOf(personId)); s.setLong(2, ((Student) person).getProgram().getId()); s.executeUpdate(); } return Long.valueOf(personId); } } catch (SQLException e) { log.error("Error while inserting person.", e); } finally { if(s!=null) try { s.close(); } catch (SQLException e) { log.error("Error while closing statement for " +
4.2
33
iBATIS
inserting person.", e); } if(cs!=null) try { cs.close(); } catch (SQLException e) { log.error("Error while closing statement for " + "inserting person.", e); } } return null; } Postup je stejný jako v předchozím případě, s řetězcem definujícím potřebnou operaci je vytvořen PreparedStatement, nastaveny parametry, jen místo metody executeQuery() vracející nalezené záznamy je použita executeUpdate(), která vrací počet ovlivněných řádků. Tato jedna metoda je používána i pro insert, delele a DDL. Pokud by namísto této metody byla zavolána addBatch(), dojde pouze k přidání příkazu do dávky, což je slibovaná altertativa z příkladu 4.1.3 na straně 29. Po úspěšném vložení je typicky požadováno vrácení klíče nového záznamu, tím je v příkladu ukázáno volání uložených funkcní a procedur. Zavoláním prepareCall(String sql) je získán CallableStatement, který je možné používat stejným způsobem jako PreparedStatement nebo Statement. JDBC API obsahuje pro získávání vygenerovaných klíčů po vložení záznamu metodu getGeneratedKeys() na Statement. V příkladu není použita, neboť není driverem pro HSQLDB podporována, je tedy použito náhradní řešení s využitím funkce identity(). Podobně lze použít i curval na sekvenci, pokud jsou na DBMS podporovány a využívají se pro generování klíče. V příkladě je dále řešena dědičnost, pokud jde o uložení studenta, musí být vložen záznam i do tabulky studentů.
4.2
iBATIS
Druhou srovnávanou technologií je iBATIS. Jedná se o rozsahově malý, po funkční stránce ovšem zajímavý rámec vybudovaný jako další vrstva nad JDBC. Svou filosofií se zaměřuje na data a optimalizaci práce se SQL příkazy v Javě. Objektově relační mapování (ORM) není a pravděpodobně ani nikdy nebude v jeho zorném poli. Hlavním přínosem je vytažení všech SQL příkazů přehledně spolu s konfigurací do XML souborů a zakrytí opakujícího se kódu na získaní spojení, vytvoření PreparedStatement a uvolnění prostředků po provedení příkazu. Část rámce, která se zabývá uvedeným přínosem, se nazývá iBATIS Data Mapper (SQL Maps) a je zkoumána v této práci. Druhé součásti rámce iBATIS se práce
4.2
iBATIS
34
nevěnuje, tou součástí je iBATIS Data Access Objects. Dále v textu se označením iBATIS myslí část SQL Maps. Zázemí iBATIS vznikl jako soubor pomocných tříd pro JDBC, které používal jeho autor, Clinton Begin. V roce 2002 se z těchto zdrojových kódů stává Open Source projekt pod licencí Apache a je používán tisíci vývojářů, podle slov jeho autora (Begin, Meadors, Goodin, 2004, předmluva). V době vzniku této práce je dostupná verze 2.3.4 pro Javu ve verzi 1.5 a novější. Připravuje se verze 3.0 s řadou vylepšení (např. generování SQL podle rozhraní a konvencí), termín vydání je ale nejasný. Na domovských stránkách (iBATIS, 2006) tohoto Apache projektu je dostupná kvalitní dokumentace v rozumném rozsahu. Lze zde i najít JavaDoc jak pro uživatele, tak vývojáře. V tištěné podobě je dostupná kniha iBATIS in Action (Begin, Meadors, Goodin, 2004). Svým přístupem tento rámec umožňuje ve vývojovém týmu lépe rozdělit role databázového specialisty a vývojáře v Javě. Výkonově problémové dotazy tak může snadno konzultovat nebo opravovat odborník i bez znalosti programování v jazyce Java. Otevřenost Přechod na jiný RDBMS je limitován pouze dostupností příslušného JDBC driveru, který SQL Maps interně použijí. V optimálním případě je nutné pouze změnit XML konfiguraci. Pokud jsou používané nějaké specifické vlastnosti databáze, je možné provádět potřebné změny bez překompilování zdrojových kódů snadno přímo v XML. Případně je možné elegantně mít verze XML souborů pro používané RDBMS uložené na různých místech a pomocí property souboru8 určit požadovanou verzi. Jen ve vzácných případech má změna RDBMS takové dopady, aby byl nutný zásah i do zdrojových kódů v Javě. iBATIS a jeho principy jsou v době psaní této práce použitelné pro programátory v Javě, .NET a Ruby. Pro „standaloneÿ aplikace a testování je možné použít jednoduchý „poolÿ spojení, který je součástí iBATIS knihoven. V serverovém prostředí není problém změnit XML konfiguraci a používat DataSource poskytovaný serverem (stejně jako je popsáno v části o JDBC). 8
Takové použití umožňuje udržovat jen jeden konfigurační XML soubor pro různé prostředí (např. vývojové, testovací, integrační nebo produkční), nastavení specifická pro jednotlivá prostředí jsou vytažena do menšího a přehlednějšího property souboru.
4.2
iBATIS
35
Výkon iBATIS je další vrstvou aplikace nad JDBC a jako taková si část prostředků zabírá pro vlastní režiji. Přínosem je tedy zvýšení produktivity práce vývojářů, kterým tvorbu perzistentních aplikací rozhodně ulehčuje. Z příkladů je patrné, že prvotní přidání nových SQL příkazů a jejich zavolání je jednoduché, je tomu tak díky automatickému mapování. Při jeho použití ovšem rámec musí část potřebných metadat zjišťována pomocí reflexe při startu nebo až při prvním vyhodnocování příkazů. Části téhle režie je možné se vyhnout zapsáním komplexnějších metadat přímo k SQL příkazům v XML souborech, tím se ale ztrácí část vývojového času. Výhodou je možnost doplnění těchto metadat až časem, kdy se ukáže, že jsou opravdu potřeba nebo kdy to fáze projektu umožní. Taková změna je záležitostí pouze XML, zdrojových kódů se vůbec nedotkne. Stejně jako ostatní optimalizace popsané v této části s vyjímkou dávek. Ještě snadněji lze používat lazy loading. Při definování metadat na zpracování výsledku lze určit, jakým dotazem s použitím hodnot z definovaných sloupců lze data dotáhnout. Takto jsou vlastně mapovány vztahy mezi objekty. Použití je patrné z příkladu 4.2.9 na straně 46. Využívání lazy loading lze povolit nebo zakázat pouze globálně, tedy pro všechny dotazy hromadně. Při zapnutí je dále nutné mít přidanou knihovnu CGLIB, pomocí které jsou za běhu generovány proxy objekty k dodatečnému dotažení dat. Pokud není knihovna dostupná, jsou data dotažena okamžitě a vzniká tak N + 1 problém. Pro řešení N + 1 nabízí iBATIS dvě varianty, první je pomocí mapování určit, kdy vytvářet novou instanci jako výsledek jednoho hromadného dotazu, který najednou načítá jak hlavní entitu, tak všech N jeho přidružených záznamů. Druhou možností je implementace vlastního RowHandler, kterým je výstup zpracován řádek po řádku pomocí vlastního zdrojového kódu. Tato varianta znamená větší náročnost pro programátora, který musí zpracování psát, dává mu však možnost výsledek zpracovat efektivněji. Při takovém použití je ušetřena hlavně paměť, neboť existuje v čase jen jedna instance pro aktuálně prováděný řádek9 , ne N + 1 instancí pro celý výsledek. Stejný příklad 4.2.9 také ilustruje použití cachování. iBATIS jako datově orientovaný umožňuje dočasně držet výsledky dotazů podle parametrů (např. Hibernate si pamatuje na úrovni instancí a jejich identifikátorů i mezi dotazy). Pokud je tedy opakovaně hledán například studijní program s určitým klíčem, druhý a následující požadavek už nepotřebuje volání databázového stroje a je místo něho vrácen zapamatovaný výsledek prvního požadavku. Součástí definice takové cache je i určení, kdy má být celá vyprázdněna (typicky nedotazovací DML na stejných datech) a jakou politikou je udržována její rozumná velikost. 9
V daném okamžiku jich je pravděpodobně více, protože dřívější nepotřebné instance čekají ještě na úklid přes garbage collector. Paměťová náročnost tohoto řešení je ovšem nesporně menší.
4.2
iBATIS
36
SQL Maps jako obal na JDBC zpřístupňují i jeho dávkové zpracování, děje se tak pro programátora v zajímavě jednoduché podobě, kdy se pouze začne nová transakce a oznámí začátek a konec dávky, zbytek kódu je stejný jako v nedávkové podobě. Označení konce dávky není nutné, pokud hned následuje potvrzení transakce (dávka je potvrzením ukončována automaticky). Použití ukazuje příklad 4.2.7 na straně 43. Podpora objektových principů Mapování výsledků dotazu na proměnné instancí je naznačeno už v předchozí podsekci. Pomocí stejného mapování lze snadno řešit i vztahy mezi objekty, stačí pouze doplnit jméno dotazu, kterým se potřebné data načtou a odkud se mají vzít vyhledávací parametry. Jasněji je to vidět v příkladu 4.2.9 na straně 46. iBATIS na rozdíl od JDBC už určitou podporu dědičnosti objektů má. V mapování výsledků je možné se odkázat na nadřazené mapování, které je doplňováno. Odkazující se mapování tedy řeší svoje sloupce a navíc i sloupce popsané v odkazovaném mapování. Hierarchie mapování je použitelná i mimo dědičnost, může ale korespondovat hierarchii dědičnosti použitých JavaBean. Podpora dědičnosti se ovšem týká jen dotazů, vkládání, modifikace nebo mazání musí být řešeno pro každou tabulku zvlášť. U dotazů je podpora včetně polymorfismu, pomocí tzv. diskriminátoru lze určovat, které mapování se pro zpracovávaný řádek použije a jaké třídy se vytvoří instance. Použití ilustruje příklad 4.2.9 na straně 46. Složitost V závislosti na zkušenostech a znalostech si lze iBATIS osvojit v řádu minut až hodin, speciálně pokud jsou už předcházející zkušenosti s perzistencí. Na domovských stránkách (iBATIS, 2006, Dokumentace) lze najít stručný tutorial o devíti stranách a šedesát tři stran delší vývojářskou příručku, která je pro studium dostatečná, stručnější a lepší než starší rozsáhlá kniha iBATIS in Action (Begin, Meadors, Goodin, 2004). Pro zvládnutí iBATIS se předpokládají znalosti jazyka Java, DML a DDL příkazy jazyka SQL, povědomí o transakčním zpracování, XML a případně zkušenosti s uloženými procedurami. Pro použití je nutné v parametru classpath doplnit cestu ke knihovně s používaným driverem a povinnou knihovnou iBATIS. Ve většině případů se použijí ještě některé volitelné knihovny. Následující tabulka uvádí velikosti knihoven používaných v této práci. Vzorový příklad V této části jsou komentované ukázky kódu, které řeší některé zajímavé partie příkladu zadaného v sekci 3.2 pomocí rámce iBATIS.
Základem všeho je konfigurační soubor, jeho jméno je volitelné, ale typicky se používá SqlMapConfig.xml. Následuje ukázka konfigurace pro tuto práci. Příklad 4.2.1 (Základní konfigurace iBATIS) <sqlMapConfig>
<sqlMap resource="Test.xml"/> <sqlMap resource="Program.xml"/> <sqlMap resource="Persons.xml"/> První tag properties není povinný a určuje, kde hledat soubor s definovanými property, na které se dále v souboru odkazuje konstrukcí ${klíč}. Soubor lze zadat přes atribut resource, kdy se hledá na classpath, nebo přes url, kdy je hodnotou místo uložení. Následuje opět nepovinný tag settings, který definuje globální nastavení pro iBATIS. Kompletní výčet možných atributů a výchozí hodnoty lze najít ve vývojářské příručce. V příkladu je postupně nastaveno: používání CGLIB pro generování proxy objektů, používání opožděného dotahování (právě přes zmiňované proxy) a používání prostoru jmen v definičních souborech se SQL příkazy. V další části je konfigurováno připojení na databázi (u serverů je zde typicky definováno JNDI jméno DataSource), v uvedém případě je používán interní implementace iBATIS SimleDataSource, která pro inicializaci používá právě zmiňované property přes ${klíč}. Poslední část je výčet souborů, které obsahují SQL příkazy, jejich pojmenování a členění je na konvencích projektů. Lokaci lze opět definovat přes atribut resource nebo url. S takovouto konfigurací je možné inicializovat SglMapClient, který je základem veškeré práce s iBATIS v javovském programu. Příklad 4.2.2 (Inicializace iBATIS) private static SchoolUtil school; private static SqlMapClient sqlMapper; public void create() { try { Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml"); sqlMapper = SqlMapClientBuilder.buildSqlMapClient( reader, CommonUtil.getJDBCproperties()); reader.close(); school = new SchoolUtilIbatisImpl(sqlMapper); } catch (IOException e) { log.error("Error while creating Ibatis sql mapper.", e); }
4.2
iBATIS
39
} SqlMapClientBuilder.buildSqlMapClient(Reader, Properties) je statickou metodou na vytvoření vláknově bezpečné instance určené pro použití během celého běhu programu, tedy typického kandidát na použití návrhového vzoru „ jedináčekÿ. Existuje i verze bez Properties, když není nutné používat dynamické hodnoty v konfiguraci. Následující příklad ukazuje brilantnost SQL Maps a automatického mapování (jedná se o soubor Program.xml). Příklad 4.2.3 (Jednoduchá ukázka select v iBATIS) <sqlMap namespace="Program"> <select id="selectProgramById" resultClass="Program" parameterClass="long"> select id, year, season, name, code from program where id = #value# V příkladu je minimum10 , co je nutné napsat pro načtení studijního programu, o zbytek se postará automatické mapování sloupců na atributy se stejným jménem. Pokud se jméno sloupce liší od odpovídajícího atributu, používá se SQL přejmenování sloupce ve výsledku pomocí klíčového slova as. Uvedený dotaz je definovaný ve jmenném prostoru Program. Pro mapování výsledku je nutné určit třídu, jejíž instance se má vytvářet pro každý vrácený řádek (atribut resultClass). Pro jména standardních tříd existují aliasy (viz dokumentace) a je možnost definovat aliasy i na vlastní třídy. Příklad 10
Skutečné minimum je použití * místo výčtu sloupců, které by bylo ekvivalentní uvedenému zápisu. Pro účely tohoto příkladu se to však nehodí a navíc autor této práce je odpůrce používání * v dotazech v produkčním kódu.
4.2
iBATIS
40
ukazuje alias na třídu cz.mendelu.pef.kotrla.dp.ibatis.ProgramImpl, na kterou je pak možné se stručně odkazovat pomocí Program. Podobně jako třídu výsledku je vhodné definovat i třídu se vstupními parametry (atribut parameterClass). V dotazu je místo, kde se má doplnit hodnota parametru označena jako #jméno#. V tomto případě se čeká na vstupu obal na primitivní typ, jméno tak může být libovolné. Lépe je mapování parametrů vidět v dalších příkladech. V XML definovaný příkaz s id selectProgramById je samozřejmě nutné použít v Javě, jak ukazuje následující příklad. Příklad 4.2.4 (Zavolání select v iBATIS) public Program findProgram(long id) { Program ret = null; try { ret = (Program) sql.queryForObject( "Program.selectProgramById", Long.valueOf(id)); }catch (SQLException e) { log.error("Error while getting program by id.", e); } return ret; } sql v ukázce je instancí SqlMapClient vytvořené v příkladu 4.2.2. Dotaz definovaný v předchozím příkladě je volán pomocí metody queryForObject, neboť je očekáván právě jeden objekt. Parametry metody jsou řetězec s id příkazu včetně jmenného prostoru (v konfiguraci je zapnuto jeho používání) a objekt s parametry pro příkaz. V tomto případě se jedná o standardní obal s hodnotou primárního klíče. Mapování parametrů je vysvětleno v následujícím příkladě. Příklad 4.2.5 (Ostatní DML v iBATIS) <delete id="deletePersonById"> delete from person where id = #id# insert into person( firstname, surname )values( #firstName#, #surname# );
4.2
iBATIS
41
<selectKey resultClass="long" keyProperty="id"> call identity() insert into student( id, id_program )values( #id#, #program.id# ); update person set firstname = #firstName#, surname = #surname#, version = version +1 where id = #id# and version = $version$; Dotazy se definují v tagu select. Tento příklad ukazuje, že ostatní DML operace mají také své tagy. Prvním je mazání, kde úmyslně není určena třída vstupního parametru a parametr je pojmenován id. Tím je možnost tento příkaz volat jak s hodnotou klíče, tak s celou instancí osoby, která má klíč nastaven. U vkládání osoby je zajímavé použití tagu selectKey, kde se definuje jak získat automaticky generovaný klíč a kam ho nastavit. Tento tag je možné použít jen uvnitř tagu insert. Může se nacházet před nebo po příkazu na vložení, podle toho, kdy se má klíč zjišťovat. Například pokud je požadováno získaní klíče ze sekvence před vložením záznamu, bude tag umístěn před vkládací příkaz. Na vkládání je dále vidět mapování parametrů, uvedeny jsou jména atributů třídy nesoucí vstupní parametry. Pokud je atribut objektem, lze se odkazovat i na jeho atributy (viz #program.id#). V poslední modifikační operaci je ještě ukázána další možnost mapování parametrů a to $jméno$. U takových parametrů dochází k textové substituci parametru ještě před vytvářením PreparedStatement, není tedy vhodné je používat bez důvodu. Například v uvedeném příkladě nemá použití žádný smysl a je uvedeno jen kvůli kompletní ilustraci možností mapování. Použití by mělo smysl při dynamickém generování příkazů, kde například u výčtu sloupců nebo jmen tabulek lze použít jen tento způsob mapování parametrů. Následující příklad ukazuje, jakým způsobem jsou operace volány v Javě.
4.2
iBATIS
42
Příklad 4.2.6 (Zavolání ostatních DML v iBATIS) public boolean removePerson(long id) { boolean ret = false; try { ret = sql.delete( "Persons.deletePersonById", Long.valueOf(id) ) == 1; }catch (SQLException e) { log.error("Error while removing person by id.", e); } return ret; } public Long storePerson(Person person) { if(person==null) throw new IllegalArgumentException("Can’t store null person."); if(person.getId()==null) { // založení nového Object key = null; try { key = sql.insert("Persons.insertPerson", person); if(person instanceof Student) sql.insert("Persons.insertStudent", person); } catch (SQLException e) { log.error("Error while inserting person", e); } return (Long) key; } else { // aktualizace starého try { if(sql.update("Persons.updatePerson", person)!=1) { throw new OptimicticLockException(); } } catch (SQLException e) { log.error("Error while updating person", e); } return person.getId(); } } Stejně jako má každá DML operace svůj tag v XML souborech, mají nedotazovací operace i svou metodu, kterou jsou prováděny, jak je patrné z příkladu. Metoda
4.2
iBATIS
43
insert vrací klíč založeného záznamu, metody delete a update vrací počet modifikovaných záznamů. Toho je v případě update využito k implementaci optimistického zámku a vyhození výjimky v případě konkurenční modifikace stejného záznamu. Více je problematice optimistických zámků věnována dřívější část 2.2 na straně 13. Další příklad ukazuje použití dávkového zpracování. Příklad 4.2.7 (Dávky v iBATIS) public void prepareData() { try { sqlMapper.startTransaction(); sqlMapper.update("Test.createDBObjects"); Program t = new ProgramImpl( Long.valueOf(1), "2009", "LS", "Java Developers", "JAVA_DEV"); sqlMapper.insert("Program.insertProgram", t); sqlMapper.commitTransaction(); // před dávkou musí začít transakce sqlMapper.startTransaction(); sqlMapper.startBatch(); StudentImpl jNovak = new StudentImpl(); jNovak.setFirstName("Jan"); jNovak.setSurname("Novák"); jNovak.setProgram(t); sqlMapper.insert("Persons.insertPerson", jNovak); Student stary = new StudentImpl(); stary.setFirstName("Marek"); stary.setSurname("Starý"); stary.setProgram(t); sqlMapper.insert("Persons.insertPerson", stary); Student mNovak = new StudentImpl(); mNovak.setFirstName("Marek"); mNovak.setSurname("Novák"); mNovak.setProgram(t); sqlMapper.insert("Persons.insertPerson", mNovak); sqlMapper.insert("Persons.insertStudent", jNovak); sqlMapper.insert("Persons.insertStudent", stary);
4.2
iBATIS
44
List batchResult = sqlMapper.executeBatchDetailed(); // commit by dávku provedl taky sqlMapper.commitTransaction(); } catch (Exception e) { log.error("Error while preparing data.", e); } } Dávky v iBATIS začínají novou transakcí (metoda startTransaction()) a zavoláním metody startBatch(). Poté jsou zapamatovány všechny operace a hromadně jsou provedeny voláním metod executeBatch, executeBatchDetailed nebo commitTransaction. Interně jsou operace seskupeny do poddávek podle id volané operace se zachováním pořadí. Provedení dávky pak sestává z postupného provedení poddávek. Uvedený příklad sestává z dvou poddávek, v první jsou vloženy záznamy osob, v druhé záznamy studentů. Kdyby se vkládal záznam o studentovi hned po vložení záznamu o osobě, dávkové zpracování v tomto příkladu by ztratilo smysl, protože by došlo k rozdělení na pět poddávek, kdy by každá obsahovala právě jednu operaci, na rozdíl od uvedeného zápisu by však byla korektní a funkční. Uvedený příklad nefunguje, protože při dávkovém zpracování se id osoby získá až při volání metody executeBatchDetailed(), tedy po přidání příkazu na vložení osoby, které tak neuvádí potřebnou hodnotu klíče. Rozdílem mezi metodami na provedení dávky je právě poskytnutí detailu o provedení těchto poddávek, první vrací pouze počet celou dávkou ovlivněných řádků (některé JDBC drivery takové informace ale nemusí poskytovat), druhá pak vrací detailní informace o každé poddávce, kolik ovlivnila řádků nebo kde nastala chyba. Následující příklad ilustruje, jak elegantně je možné pomocí Sql Maps realizovat dynamické dotazy. Příklad 4.2.8 (Dynamický select v iBATIS) <sql id="selectPersonAndStudent"> select p.id as id, p.firstname, p.surname, p.version, case when s.id is null then ’n’ when s.id is not null then ’y’ end as is_student, s.id_program <sql id="fromPersonLeftStudent"> from person p left join student s on p.id=s.id
4.2
iBATIS
45
<select id="selectPersonByName" parameterClass="Person" resultMap="ResultPersonMap"> firstname = #firstName# surname = #surname# public List findPersons(String firstName, String surname) { Person p = new StudentImpl(fisrtName, surname); List ret = null; try { ret = sql.queryForList("Persons.selectPersonByName", p); }catch (SQLException e) { log.error("Error while getting person by name.", e); } return ret; } První dvě části XML jsou tzv. fragmenty, které umožňují redukovat duplicitu v zápisu operací a usnadnit hromadnou změnu modifikací na jednom místě. Tyto fragmenty je možné pomocí tagu include vkládat do definicí SQL operací. Vložení je vidět v definici selectPersonByName, kde je napřed vložen výčet požadovaných sloupců a následně způsob spojení zdrojových tabulek. Následuje dynamické poskládání podmínek podle vstupních parametrů. Pro vyhledání se použijí jen ty části jména, které nejsou null, atribut prepend určuje, jaký řetězec se má předřadit, pokud dynamická část generuje nějaký výstup. V případě isNotNull se ale řetězec předřazuje jen, pokud vytvořený výstup není prvním v pořadí. Díky tomu pro každou kombinaci vstupních parametrů vzniká syntakticky korektní SQL dotaz. Pro prázdné obě části jména je proveden dotaz bez omezující podmínky, který vrátí všechny osoby. Pokud by podobný dotaz měl řešit vývojář sám v JDBC, nevyhnul by se složité spleti podmínek nebo by si pomohl trikem, kdy první podmínka je 1=1. Dynamické
4.2
iBATIS
46
dotazy umožňují použít celou řadu dalších konstrukcí, které může zaujatý čtenář najít ve vývojářské dokumentaci. Konec příkladu ukazuje zavolání operace pomocí metody queryForList(), lze totiž očekávat více záznamů (nalezených osob). Dotaz je zároveň polymorfní a je ho tak nutné doplnit o explicitní mapování (v XML je odkazováno hodnotou atributu resultMap), které je uvedeno v následujícím příkladu. Příklad 4.2.9 (Dědičnost, lazy loading a vztahy v iBATIS) <subMap value="y" resultMap="ResultStudentMap"/> Tento příklad obsahuje několik oblastí najednou. Prvně uvádí efektivnější alternativu k implicitnímu automatickému mapování, které bylo popsáno u příkladu 4.2.3. Toto explicitní mapování v podobě tagu resultMap spojuje jména sloupců ve výsledku dotazu s jmény atributů ve vytvářené třídě. V druhé řadě ukazuje mapování dědičnosti, kde mapa ResultStudentMap rozšiřuje (atribut extends) mapu ResultPersonMap. Ta tagem discriminator určuje, jaké mapování se má na řádek použít v závislosti na hodnotě sloupce uvedeného v atributu column. Pokud není hodnota ve výčtu subMap nalezena, není žádné další mapování použito. V příkladu této práce je toto chování využito na rozlišení osob a studentů. Poslední je definování vztahů a jejich možnost použití pro opožděné nahrávání. Tímto způsobem je mapován atribut program třídy osob, kde v tagu result atribut select odkazuje přes id na dotaz, který objekt nebo kolekci objektů nahraje (opožděně, je-li funkčnost zapnuta) a atribut column odkazuje na zdroj parametrů pro uvedený dotaz.
4.2
iBATIS
47
Poslední příklad ilustruje dočasné pamatování výsledků dotazů. Příklad 4.2.10 (Cache v iBATIS) <property name="reference-type" value="WEAK"/> <select id="selectProgramById" resultClass="Program" cacheModel="programCache"> ... ... Pro cache je nejdříve nutné nastavit její vlastnosti, to se děje prostřednictvím tagu cacheModel s atributy: • id, který definuje jméno cache používané jako hodnota atributu cacheModel u dotazů, • type určující politiku pro velikost, možné jsou například podle dostupné paměti, LRU nebo FIFO (podrobnosti ve vývojářské dokumentaci), • readOnly určuje, jestli aplikace data z cache i modifikuje (false znamená, že modifikuje), • serialize určuje, zda má být každý objekt před vytažením zkopírován (používá se při modifikaci dat z cache). Dále se určuje, kdy mají být data z cache zahozena. Je možné nastavit jak časový interval (tag flushInterval), tak událost, která má zahození vyvolat. Těmito událostmi jsou modifikující operace, které jsou vyjmenovány přes tagy flushOnExecute. Při zahazování je vyprázdněna vždy celá cache. Každý typ má své specifické nastavení pomocí vnořených tagů property.
4.3
4.3
Hibernate
48
Hibernate
Hibernate je asi nejznámějším ORM rámcem, který se díky volné licenci těší velké oblibě a je hojně používán. Umožňuje pracovat s databází bez napsání jediného SQL příkazu, ty rámec generuje automaticky. Programátorovi však psát vlastní SQL příkazy v případě potřeby umožní. To vše díky podrobnému mapování entit a jejich vlastností na relační databáze. Pomocí obalení přístupu k datům a cachování změn s opožděným zápisem finální podoby až s ukončením a potvrzením celého požadavku optimalizuje přístupy do databáze. Svým API umožňuje programátorům rychle psát přehledný kód bez zbytečného řešení notoricky se opakujících úkolů s perzistencí. Tento rámec lze považovat za ukázku návrhového vzoru Data Access Object (DAO), který je mimo jiné popsán v knize Core J2EE Patterns (Alur, Crupi, Malks, 2003, kap. 8). Zázemí Otcem tohoto rámce je Gavin King, první verze jím byla vystavena v roce 2001, od té doby se Hibernate rozrostl do impozantních rozměrů. Od roku 2003 je King zaměstnán firmou JBoss a stará se o další vývoj tohoto zajímavého rámce. V současné době tedy spadá tato technologie pod společnost Red Hat. Nyní se Hibernate skládá z jádra a několika rozšíření. Jde o podporu anotací zavedených od Javy 5 (Hibernate Annotations), obal jádra pro implementaci Java Persistence API (JPA) definované v EJB 3.0 (Hibernate EntityManager), přístupu k distribuovaným datům ve více databázích (Hibernate Shards), validaci JavaBean vlastností (Hibernate Validator), full-text vyhledávání (Hibernate Search) a podpůrné nástroje pro použití (Hibernate Tools). Pro srovnání v této práci je používáno pouze jádro a podpora anotací11 . Od září minulého roku je k dispozici jádro ve verzi 3.3.1.GA, které je použitelné v Javě 1.4 a novější. Masivní rozšíření tohoto rámce je možné díky tomu, že se jedná o volný software pod licencí LGPL v2.1, která umožňuje jeho použití i pro komerční projekty. O Hibernate vyšlo několik rozsáhlejších publikací, oficiálně uváděnými jsou Hibernate In Action (Bauer, King, 2004) věnující se dnes už staré verzi a její novější revize Java Persistence with Hibernate (Bauer, King, 2007), která je obsáhlejší a poskytuje aktuální informace. Vedle těchto zdrojů je pro studium výhodné využít rozsáhlou dokumentaci přístupnou z domovské stránky projektu (Hibernate, 2004, Dokumentace). Kromě online nebo tištěné dokumentace, ukázkového příkladu a JavaDoc dokumentace je na uvedeném místě k dispozici i „wikiÿ, obsahující řadu rad a vzorů použití. 11
Anotace jsou vybrány kvůli úspoře místa při prezentování příkladů a k ukázce možnosti mapovat jinak než v XML, jako to dělá iBATIS. Řada projektů však před anotacemi dá přednost původnímu mapování v XML, které je možné upravovat bez nutnosti rekompilace zdrojových kódů.
4.3
Hibernate
49
Otevřenost Hibernate plně implementuje JPA, včetně akceptování mapování touto specifikací definovaného. Poskytuje dokonce funkčnosti navíc, které je samozřejmě nutné mapovat pomocí specifického API Hibernate. Takto je aplikaci poskytnuta pokročilejší funkcionalita, ovšem na úkor otevřenosti. Aplikace na JPA se specifickým mapováním Hibernate se stává na tomto rámci závislá, výměna implementace EntityManager je tak komplikovanější. Při dodržení přenositelného mapování (používaní standardů a ne specifických vlastností DBMS) je přechod na jiného poskytovatele ze všech srovnávaných technologií v této práci právě v Hibernate nejznažší. Jde o nahrazení knihovny s driverem a úpravu konfiguračního souboru (jméno driveru, JDBC nastavení pokud se nepoužívá DataSource a nastavení dialektu pro generované SQL). Následující seznam obsahuje výčet ověřených DBMS, jejichž použití spolu s Hibernate je osvědčeno certifikátem (Hibernate, 2004, Certifikace). • MS SQL 2005 • Oracle 10g • Caché 2007.1 • Teradata 6.1.1 • Ingres 2006 Mimo tohoto seznamu existuje i seznam neoficiálně podporovaných zdrojů dat podle uživatelů (Hibernate, 2004, Podporované databáze), kde lze nalézt například: • Oracle 8i a 9i, • DB2, • MS SQL 2000, • Sybase, • MySQL, • PostgreSQL, • SAP DB, • Firebird, • Informix, • HSQLDB, • Access, • Excel, • a CSV soubory. Někdy jsou při přechodu vhodné ještě další zásahy vývojáře, například když původní a nový DBMS řeší generování hodnot primárních klíčů pomocí různých strategií, zejména když nová strategie je získávání hodnot ze sekvence. JPA definuje tyto strategie: • TABLE, • IDENTITY, • SEQUENCE, • a AUTO.
4.3
Hibernate
50
Pokud je v mapování pro klíč AUTO, je automaticky zvolena jedna z ostatních strategií, která je podporována a primární pro použitý databázový stroj. S automatickou strategií a zapnutou správou databázového schématu rámcem Hibernate by se tak pro nově používanou strategii automaticky založila sekvence s názvem hibernate_sequence. Většinou je však požadována kontrola nad názvy používaných sekvencí ze strany členů týmu, tak by takový přechod mezi DBMS vyžadoval i doplnění definicí použitých sekvencí a jejich vlastností včetně vlastních jmen. Vedle Javy je od roku 2005 dostupný NHibernate, rámec vybudovaný na myšlenkách Hibernate pro .NET, který je udržován komunitou vývojářů. Výkon Hibernate zvyšuje výkon aplikací pomocí cachování a flexibilního opožděného nahrávání. Cachování je vždy prováděno na Session, která představuje sadu atomických perzistentních operací (označováno jako „ jednotka práceÿ), detailněji je vysvětlena v příkladu 4.3.4 na straně 58. Kromě této první úrovně cachování je volitelně možné zapnout ještě druhou úroveň cachování, kde se udržují data pro perzistentní entity i mezi více Session. Jsou možné různé implementace této cache a různé nastavení jejího použití (např. jen pro čtení nebo i pro zápis), detaily jsou vysvětleny v knize (Bauer, King, 2007). Při použití této cache je důležité si uvědomit, že udržuje hodnoty vlastností entit, nikoliv samotné entity. Na rozdíl od cachování na Session tak nezajišťuje (a nikdy nebude) jedinečnost vrácených entit. Pro jeden identifikátor je ze Session opakovaně vrácena stejná instance (i při kombinaci dotazů a vyhledávacích metod Hibernate), z cache na druhé úrovni jsou v tomto případě vráceny různé instance. První úroveň cachování udržuje entity, se kterými se při požadavku pracuje, stejně tak změny na těchto entitách provedené. Změny nejsou propagovány do databáze průběžně, ale až jejich finální podoba sumárně na konci při potvrzení požadavku. Tím je ušetřena zbytečná komunikace na databázový stroj. Mapování všech vztahů obsahuje i určení režimu dotahování hodnoty, lze vybrat buď okamžité nahrání (děje se pomocí spojení nebo dalším dotazem), nebo odložené nahrání na přání. V druhém případě se tak šetří zbytečné dotazy, které nejsou chtěné. Nastavení z mapování je globální, programově je však možné použitý režim pro aktuálně vytvářený dotaz změnit. Typicky se tak globálně používá odložené nahrávání a u dotazů, kde je předem známá nutnost dohrání je režim změněn na okamžitý. S nastavením použití spojení se tak řeší i N + 1 problém. Hibernate při propagování změn do databáze používá dávky, jak jsou popsány už v části o JDBC. Použitím Hibernate tedy pro DML programátor používá i dávkové zpracování. Dále jsou poskytnuty možnosti dalších optimalizací, při načítání násobných vztahů do kolekcí lze stránkovat (načte se jen část dat v požadovaném rozsahu), nebo lze psát modifikační dotazy pomocí HQL, při kterých jsou data modifikována okamžitě a přímo v databázi. Ilustruje to příklad 4.3.6 na straně 60.
4.3
Hibernate
51
HQL je objektový dotazovací jazyk Hibernate podobný SQL, místo tabulek se ale používají třídy a místo sloupců jejich atributy. Místo počtu ovlivněných řádků se při provedení vrací počet ovlivněných instancí. Při provádění se uvažuje dědičnost, operace tedy odpovídajícím způsobem modifikují data podle hierarchie tříd (při voláni jedné HQL může být provedeno i několik SQL). Příbuzný JPA QL jazyk, definovaný v rámci JPA specifikace je funkční podmnožinou HQL, nepodporuje například INSERT SELECT (Bauer, King, 2007, kap. 12.2.1). Podpora objektových principů Od ORM rámce lze očekávat, že bude podporovat zkoumané objektové principy. Skutečně, vztahy Hibernate řeší za programátora, který pouze musí určit pomocí mapování, v jakém vztahu objekty jsou. Pro mapování dědičnosti se v ORM používají tři strategie přehledně znázorněné na obrázku a popsány v dalších odstavcích. Dle JPA anotací se jedná o: • InheritanceType.TABLE_PER_CLASS, • InheritanceType.SINGLE_TABLE, • a InheritanceType.JOINED.
Obr. 4: ORM mapování dědičnosti
U první strategie je každá třída mapována na samostatnou tabulku, která má sloupec pro každou netransientní vlastnost třídy včetně vlastnosti perzistentních předků. U této strategie nelze provádět polymorfní dotazy, neboť jsou data v samostatných tabulkách. Druhá strategie je opačná, celá hierarchie tříd je mapována na jednu tabulku, která má sloupec pro každou netransientní vlastnost všech perzistentních tříd z hierarchie. Ve sloupcích pro vlastnosti jiných tříd tak musí být ukládána prázdná
4.3
Hibernate
52
hodnota a nelze využívat not null omezení DBMS pro kontrolu dat. Navíc je v tabulce sloupec pro tzv. diskriminátor, který slouží k určení třídy uložených dat. Nad takto uloženými daty je už možné provádět polymorfní dotazy a jejich provedení je rychlé (díky uložení všech dat v jedné tabulce). Poslední strategie je používána i na příkladu této práce. Jedná se o mapování tříd na normalizovaný datový model, každá podtřída přidává v modelu svou tabulku, ve které jsou na sloupce mapovány netransientní vlastnosti této podtřídy (ne vlastnosti poděděné z předků). Jde o strategii optimální na uložení dat, která podporuje polymorfní dotazy. Pří složitější hierarchii dědičnosti je provedení dotazu časově náročné, protože se musí použít vnější spojení na mnoho tabulek. Z popisu strategií vyplívá, že Hibernate podporuje i polymorfní dotazy. Problémové vnější spojení popisované u poslední jmenované strategie je možné u některých scénářů použití optimalizovat doplněním vlastnosti s funkcí diskriminátoru do kořenové třídy stromové hierarchie a nahrazením problémového dotazu jiným, který vrací pouze identifikátor a hodnotu diskriminátoru (použije se jen jedna tabulka). Na základě těchto informací je dalším dotazem načtena konkrétní instance (nedělají se zbytečné vnější spojení na nepotřebné tabulky pro zjištění třídy uložené entity). Složitost Podle harmonogramu je cvičení na prvotní seznámení s rámcem Hibernate rozloženo na tři dny (Hibernate, 2004, Tutoriál) a odkazuje se na knihu Java Persistence with Hibernate (Bauer, King, 2007). Vzhledem k rozsahu knihy nelze očekávat její celé přečtení v jednom dni (dle harmonogramu druhý den). Podrobné nastudování rámce z knihy a webu (Hibernate, 2004, Dokumentace a wiki) tak zabere i několik týdnů. Při stejné úvaze jako u JDBC o potřebách a vytížení programátora se čas může protáhnout na roky. Pro použití Hibernate se očekává pouze znalost programování v jazyce Java a použití XML, případně povědomí o struktuře uložení v relačních databázích, které usnadní pochopení objektově relačního mapování. Znalost SQL urychluje pochopení HQL. Hibernate je náročný co do počtu i velikosti potřebných knihoven. Pro testy této práce byly použity knihovny uvedené v následující tabulce. Vzorový příklad V této části jsou komentované ukázky kódu, které řeší některé zajímavé části příkladu zadaného v sekci 3.2 pomocí rámce Hibernate. Základem je konfigurační soubor, jeho jméno je volitelné, ale defaultně se používá hibernate.cfg.xml. Následuje ukázka konfigurace pro tuto práci. Příklad 4.3.1 (Základní konfigurace Hibernate)
4.3
org.hibernate.cache.NoCacheProvider --> <property name="cache.provider_class"> org.hibernate.cache.HashtableCacheProvider <property name="hibernate.cache.use_second_level_cache"> false <property name="show_sql">true <property name="format_sql">true <property name="hbm2ddl.auto">${hibernate.hbm2ddl.auto} <mapping class="cz.mendelu.pef.kotrla.dp.hibernate.ProgramImpl" /> <mapping class="cz.mendelu.pef.kotrla.dp.hibernate.PersonImpl" /> <mapping class="cz.mendelu.pef.kotrla.dp.hibernate.StudentImpl" /> XML konfigurace ukazuje některé možné vlastnosti (tagy property) pro SessionFactory (pozdější příklad 4.3.3) a určení tříd, které mají být rámcem Hibernate automaticky perzistentní (tagy mapping). Pro nastavení používaného JDBC používáme stejné konstrukce ${klíč} jako u iBATIS, zde jsou hodnoty dynamicky brány ze systémových vlastností. Vestavěný „poolÿ má udržovat dvě spojení na databázi (pro testy této práce dostatečný počet). Stejná konstrukce ${klíč} je použita i na určení dialektu pro generování SQL příkazů. Následuje nastavení sekundární cache. Tu je možné vypnout buď vybráním prázdné implementace NoCacheProvider, nebo nastavením pravdivostní hodnoty vlastnosti hibernate.cache.use_second_level_cache. V příkladu je použit druhý způsob. Kdyby byla hodnota opačná, použila by se pro testy interní implementace Hibernate HashtableCacheProvider, která není vhodná pro produkční prostředí.
4.3
Hibernate
55
Následuje nastavení vhodné pro ladění, kde se určuje, zda se mají volané SQL příkazy vypisovat na standardní výstup (vlastnost show_sql). Příkazy jsou tak vypisovány na jednom řádku, pro lepší čitelnost lze zapnout jejich formátování (vlastnost format_sql)12 . Poslední vlastností se dynamicky určuje, co má Hibernate dělat s datovým modelem. Možné nastavení jsou: • validate – při vytváření továrny se kontroluje mapování oproti existujícímu schématu v databázi, • update – existující schéma je aktualizováno podle mapování, • create – případně existující schéma je zrušeno a vytvořeno je znovu podle mapování, • create-drop – stejné jako předchozí, navíc se se při explicitním uzavření továrny zahazuje i používané schéma. Při určování perzistentních tříd (poslední část příkladu) lze místo uvedení celého jména třídy určit knihovnu se třídami, balík obsahující třídy nebo XML soubory s mapováním. Výčet a vysvětlení všech možností při nastavování továrny lze najít na webu (Hibernate, 2004, Dokumentace) nebo v knize (Bauer, King, 2007). Následující příklad ilustruje použití anotací na mapování. Příklad 4.3.2 (Mapování pomocí anotací v Hibernate) @Entity @Inheritance(strategy=InheritanceType.JOINED) @Table(name="person") @Cache(usage=CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class PersonImpl implements Person, Cloneable { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private String firstName; private String surname; @Version private Long version; public PersonImpl() {} // setters and getters } @Entity @Table(name="student") 12
Hibernate používá optimální PreparedStatement s ? na místě parametrů. Pro ladící účely je možné nastavit úroveň logování pro interní třídy z balíku org.hibernate.type na úroveň DEBUG, tím jsou po každém příkazu vidět i použité hodnoty parametrů.
4.3
Hibernate
56
public class StudentImpl extends PersonImpl implements Student { @ManyToOne(targetEntity=ProgramImpl.class, fetch=FetchType.LAZY, optional=false) @JoinColumn(name="id_program") private Program program; public StudentImpl() {} // setters and getters } @Entity @Table(name="program") @Cache(usage=CacheConcurrencyStrategy.READ_ONLY) public class ProgramImpl implements Program { @Id private Long id; private String season; private String year; private String name; private String code; @OneToMany(targetEntity=StudentImpl.class, mappedBy="program", fetch=FetchType.LAZY) private List<Student> students; // setters and getters } Anotace @Entity označuje třídu za perzistetní. Všechny její atributy jsou automaticky mapovány na sloupce v databázi, pokud nejsou označeny @Transient. Strategii mapování dědičnosti je možné určit přes @Inheritance, uvedenou u kořenové třídy stromu dědičnosti. Jinak je použita defaultní strategie SINGLE_TABLE, kterou na potomcích nelze předefinovat. Podle jmenných konvencí se třídy mapují na stejně pojmenované tabulky, resp. atributy na stejně pojmenované sloupce. Když tato konvence nevyhovuje, lze pomocí @Table u třídy, resp. @Column nebo @JoinColumn u atributu určit vlastní jméno. U každé třídy lze určit, jaká strategie má být použita při jejím udržování v sekundární cache. @Cache je anotací pouze Hibernate, která neexistuje v JPA. U perzisteních tříd musí být určen jejich identifikátor, například anotací @Id, kterou lze ještě doplnit o @GeneratedValue pro určení, že tento identifikátor není
4.3
Hibernate
57
vkládán programově, ale má být generován pomocí databáze. Uvedení strategie generování AUTO v příkladu není nutné, neboť se jedná o defaultní hodnotu. Anotací @Version se určuje, který atribut má být používán pro optimistické zamykání. Vztah mezi studijním programem a studenty je mapován pomocí dvojice anotací @OneToMany u studenta a @ManyToOne u programu. Určení targetEntity v obou případech je nutné, protože se v příkladu používají rozhraní místo tříd, fetch nastavuje pro obě strany vztahu opožděné nahrávání, což je defaultní vlastnost vztahů v Hibernate, nikoliv v JPA. mappedBy říká, který atribut třídy na druhé straně vztahu použít pro spojení. Bez jeho uvedení je vztah jednosměrný (jen z programu na studenty) a pro jeho uložení je používána extra vazební tabulka. Jakmile je mapování a konfigurace kompletní, je možné vytvořit instanci SessionFactory. Příklad 4.3.3 (SessionFactory Hibernate) private static SessionFactory sessionFactory; private static SchoolUtilHibernateImpl school; public void create() { try { // Nastavení systémových vlastností použitých v konfiguraci Properties propToAdd = CommonUtil.getJDBCproperties(); for(Entry