Objektově relační mapování definujeme převážně pomocí XML syntaxe v souborech tomu určených. Lze ovšem použít i klasických Java 5 anotací. V naší aplikaci se budeme věnovat XML reprezentaci mapování, která je mnohem více používaná a také vyspělejší.
Hibernate poskytuje několik strategií mapování tříd na tabulky relační databáze. Nejjednoduší strategií, kterou tento rámec poskytuje, je strategie mapování jedné třídy na jednu tabulku. Tento způsob mapování funguje výborně do té doby, než chceme využít dědičnost nebo polymorfismus. Hibernate proto poskytuje další strategie, které využití dědičnosti a polymorfismu umožňují. Nyní si všechny strategie představíme podrobněji.
Nejjednodušší způsob mapování tříd. Nepodporuje dědičnost a polymorfismus.
Pomocí speciálního atributu, tzv. diskriminátoru, dokáže Hibernate rozpoznat, který objekt daná relace zachycuje, a tento objekt poté vytvořit. Povoluje polymorfismus i dědičnost.
Umožňuje používat dědičnost i polymorfismus. Narozdíl od strategie hierarchie tříd mapovaných na jednu tabulku, tato strategie produkuje naprosto normalizované databázové schéma.
Umožňuje používat dědičnost i polymorfismus. V tabulkách oproti předchozí strategii dochází k duplikaci společných atributů. Tato strategie je často používaná zejména při práci s již existujícím databázovým schématem, které nemáme možnost měnit.
U deklarací mapování si budeme vždy uvádět jen ty nejzajímavější a nutné parametry. Většina typů deklarací obsahuje mnoho nastavení, pro které však v tomto textu není prostor a kterých se dočtete v oficiální dokumentaci[1] k nástroji Hibernate.
Nyní si představíme mapování třídy User,
reprezentované souborem User.hbm.xml, pomocí
strategie jediné třídy mapované na jedinou tabulku. Další strategie si
ukážeme v pozdějších částech textu.
Pomineme-li základní vlastnosti XML souborů,
jako je definování Doctype a
kořenového elementu v podobě
<hibernate-mapping>, dostaneme se k elementu
<class>, který říká, jakouže třídu to chceme
vlastně zperzistentnit a na jakou tabulku ji chceme namapovat.
Element <class> má sadu atributů. Nyní
se krátce zmíníme o těch nejzajímavějších.
Název perzistentní třídy včetně balíku, pokud není
nastaven atribut package
elementu <hibernate-mapping>.
Název tabulky v relační databázi, na kterou se bude naše třída mapovat.
Obsahuje rozhraní, které bude objekt implementovat, pokud bude součástí líně vyhodnocovaných kolekcí či objektů.
Dokáže omezit výběr dat reprezentovaných touto třídou nějakou podmínkou.
Aktivuje / deaktivuje líné získávání dat z kolekcí a asociovaných objektů, které jsou součástí této třídy.
Hodnotou atributu name nemusí
být přímo třída, ale může jí být i rozhraní nebo třída abstraktní, jak se můžeme podívat v souboru
SportEvent.hbm.xml. Potom můžeme použít element
<subclass> pro definování jejich
implementace.
Každá vlastnost naší třídy je nějakého typu. Hibernate podporuje všechny základní javové datové typy, ale hodnotou tohoto atributu může být i objekt, kolekce objektů či vlastní datový typ. Hibernate poskytuje také množinu vlastních typů, které víceméně odpovídají základním datovým typům jazyka Java.
První vlastností mapovanou v našem XML
souboru je vlastnost id zachycující primární
klíč.
Mapovaná třída musí obsahovat deklaraci primárního klíče
databázové tabulky. Element <id> definuje
právě toto mapování.
<id column="ID" name="id" type="string"> <generator class="uuid" /> </id>
Na našem příkladu, vyjmutém ze souboru
User.hbm.xml, vidíme definici tohoto elementu,
která říká, že vlastnost id typu
String naší třídy je primární klíč, který je
v databázové tabulce OUSER reprezentován
atributem id typu řetězec. Tento element obsahuje
typické parametry každé mapované vlastnosti.
Jméno mapované vlastnosti.
Sloupec databázové tabulky, na kterou se bude daná vlastnost mapovat. Pokud není určen, jméno atributu bude shodné s názvem vlastnosti.
Hibernate typ této vlastnosti. Pokud není určen, Hibernate se ho pokusí zjistit pomocí reflexe.
Na primární klíč Hibernate neklade žádné speciální nároky. Primární klíč by měl být složen z atributu/atributů dané tabulky, jeho hodnota by v databázi neměla být nikdy null a neměla by se často měnit. Primární klíč by měl vždy jednoznačně identifikovat jednotlivé záznamy v tabulce.
Primární klíče můžeme rozdělit do dvou skupin. Jedná se o klíče nesoucí nějaký význam ve vztahu s reálným světem (natural keys) nebo uměle generované klíče (surrogate keys). Hibernate podporuje obě varianty těchto klíčů, nicméně doporučována bývá cesta klíčů uměle generovaných, kterou jdeme i my v naší aplikaci.
Element <generator> zachycuje
jednotlivé druhy generátorů primárních klíčů. Za všechny jmenujme
generátor uuid, který generuje 32 znakové
řetězce hexadecimálních čísel. Dále zmiňme generátor vytvářející
sekvenci čísel, inkrementální generátor atd.
Pomocí elementu <property> můžeme
mapovat libovolnou vlastnost tzv. primitivního datového typu.
Primitivním datovým typem máme na mysli základní datové typy jazyka
Java nebo datové typy poskytované Hibernate.
<property column="PASS"
length="100"
name="password"
not-null="false"
update="true"
insert="true"
type="string" />
Na příkladu vidíme úryvek ze souboru
User.hbm.xml, kde mapujeme vlastnost
password typu String na atribut
PASS. Maximální délka tohoto
atributu je omezená na 100 znaků a nesmí být rovna
null. Tato vlastnost je zahrnuta jak v
příkazech typu insert, tak v příkazech typu update.
Zajímavým atributem elementu
<property> může být atribut se jménem
formula, který obsahuje SQL dotaz.
Příslušná vlastnost nabývá po načtení objektu z databáze hodnot
právě tohoto vyhodnoceného dotazu.
<property name="numberOfUsers"
formula="( SELECT count(*) FROM ouser )"/>
Hibernate obsahuje další „speciální“ mapování
jednotlivých vlastností. Jedním z těchto elementů je element
<timestamp> nebo
<version>.
Element <version> je doporučený a
říká, že tabulka obsahuje verzovaná data. Toto bývá prospěšné v
případě souběžného přístupu nebo transakcí a deklaraci tohoto
elementu je nutné uvést ihned za deklaraci primárního klíče.
Hodnotou databázového atributu pak bývá vetšinou číslo nebo časové
razítko. V tomto případě můžeme použít přímo element
<timestamp>, viz.
SignPost.hbm.xml
<timestamp name="lastUpdatedDate" column="LAST_UPDATED_DATE"/>
Pokud používáme Hibernate pro perzistenci našich objektů, musíme rozlišovat dva druhy asociací mezi nimi. Jedním typem jsou jednosměrné asociace, druhým typem pak obousměrné asociace. Pokud využijeme jednosměrnou asociaci mezi dvěma objekty, potom pouze jeden z nich bude mít odkaz na ten druhý. Při obousměrných asociacích mají oba objekty referenci na svůj protějšek.
Nejběžnější typ asociace mezi třídami je zachycen elementem
<many-to-one>. V relačním modelu tuto
vlastnost popisujeme cizím klíčem jedné tabulky ukazujícím na primární
klíč tabulky jiné.
Jak jsme si již řekli, jednosměrná relace
many-to-one je deklarována stejnojmeným
elementem, jak si ukážeme na části souboru
Match.hbm.xml.
<many-to-one name="homeCompetitor"
class="Competitor"
column="HOME_COMPETITOR_ID" />
Tato deklarace zachycuje asociaci mezi zápasem (reprezentovaný
třídou Match) a jeho soutěžícími
(Competitor) s tím, že každý zápas obsahuje
vlastnost homeCompetitor
reprezentující soutěžícího označeného jako domácí, do které je
asociovaný soutěžící namapován. Podobnou deklaraci obsahuje soubor
Match.hbm.xml i pro soutěžícího označeného jako
host.
Zde je potřeba uvést, že tato deklarace může obsahovat
parametr cascade , který, pokud obsahuje jinou hodnotu než none, propaguje určité operace až na samotný
asociovaný objekt. Může nabývat kombinací hodnot rovných názvům
základních Hibernate operací, jako jsou
persist, merge,
delete, save-update,
evict, atd. nebo speciálnímu
delate-orphan či all.
Pozměněné mapování many-to-one s parametrem
cascade můžeme najít v souboru
PlayingSystemPhase.hbm.xml. Tento parametr se
netýká pouze deklarace many-to-one, ale je
společný pro všechny deklarace asociací a kolekcí na základě
libovolného mapování.
Asociaci jedna na jednu mezi třídami můžeme vyjádřit vícero
způsoby. My si zmíníme následující, který můžete vidět v akci v
souboru SportEvent.hbm.xml.
<many-to-one name="playingSystem"
class="PlayingSystem"
column="PLAYING_SYSTEM_ID"
unique="true"
/>
Tato definice říká: namapuj objekt typu
PlayingSystem na vlastnost playingSystem třídy
SportEvent na základě jeho cizího klíče
uvedeného v atributu PLAYING_SYSTEM_ID. Tento objekt musí být
jedinečný z důvodu uvedení atributu unique s hodnotou true,
proto se jedná o asociaci jeden na jednoho.
Protože my v tomto případě vyžadujeme oboustranou asociaci,
soubor PlayingSystem.hbm.xml musí obsahovat
následující deklaraci a třída PlayingSystem
vlastnost sportEvent.
<one-to-one name="sportEvent"
property-ref="playingSystem"/>
Tato deklarace říká, že hrací systém
(PlayingSystem) je v asociaci se sportovní
událostí (SportEvent) s poměrem jeden na
jednoho. Sportovní událost je mapována do atributu
sportEvent na základě cizího klíče hracího
systému uvedeného v tabulce zachycující sportovní událost. Aby toto
fungovalo, musí databáze podporovat cizí klíče.
Element <component> mapuje vlastnosti
objektu asociované třídy do tabulky rodičovského objektu. Tuto
vlastnost si ukážeme na souboru
Competitor.hbm.xml, kde pomocí tohoto mapování
deklarujeme adresu našeho soutěžícího.
<component name="address" class="Address"> <property name="street" column="street" type="string" not-null="true" /> <property name="number" column="number" type="string" not-null="true" /> <property name="city" column="city" type="string" not-null="true" /> <property name="zip" column="zip" type="string" not-null="true" /> <property name="country" column="country" type="string" not-null="true" /> </component>
Pokud v našem projektu chceme využít polymorfismus nebo dědičnost, musíme využít strategiií mapování hierarchie tříd přes jednu tabulku nebo strategii mapování podtřídy na jednu tabulku.
V souboru SportEvent.hbm.xml najdeme
mapování třídy SportEvent na databázovou
tabulku SPORT_EVENT. Jiné oproti klasickému
mapování je, že třída SportEvent je rozhraní
a tudíž nejde přímo instanciovat. Mapování proto obsahuje deklaraci
elementu <subclass>, která definuje
implementace tohoto rozhraní.
<subclass name="SimpleTournament" discriminator-value="TOURNAMENT"></subclass> <subclass name="SimpleLeague" discriminator-value="LEAGUE"></subclass> <subclass name="SimpleRace" discriminator-value="RACE"></subclass>
Na příkladu je uvedena implementace tohoto rozhraní v podobě
tříd SimpleTournament,
SimpleLeague a
SimpleRace. Hodnota diskriminátoru určuje, jaká z podtříd bude instanciována.
Pokud využijeme potenciálu mapování dědičnosti pomocí
deklarace <joined-subclass>, dosáhneme
optimalizovaného databázového schématu. Vlastnosti každé podtřídy
jsou při této strategii mapování mapovány do speciální tabulky tak,
jak ukazuje následující schéma.
V SignPost.hbm.xml definujeme potomka
třídy SignPost pomocí elementu
<joined-subclass> následovně.
<joined-subclass name="SignShopPost"
table="SIGNPOST_ADDRESS">
<key column="id"/>
<property name="address" type="string"/>
<property name="phone" type="string"/>
</joined-subclass>
Jak jste si jistě všimli, v tomto případě nemusíme používat
diskriminátor pro určení, která z potříd se má instanciovat, nicméně
musíme deklarovat pomocí elementu <key>
atribut udržující identifikátor rodičovského objektu.
Tento přístup sice optimalizuje databázové schéma, nicméně dotazy nad tímto schématem jsou složitější než za použití strategie mapování více tříd na jednu databázovou tabulku.
Element <key> v tomto případě
definuje cizí klíč závislé tabulky, který ukazuje na primární klíč
tabulky rodičovské.
Pokud bychom v tomto případě využili strategii mapování více tříd na jednu databázovou tabulku, naše databázové schéma by vypadalo následovně.
Polymorfní asociace je asociace, která může odkazovat na
instance podtříd třídy, v jejímž XML mapování
byla explicitně uvedena. Následující mapování ve skutečnosti
neasociuje objekt typu SportEvent, ale
některou z jejich implementací. Mapování třídy
SportEvent jsme si ukázali v podkapitole
3.2.5.1.
<many-to-one name="sportEvent"
class="SportEvent"
column="SPORT_EVENT_ID" />
Mapování kolekcí je stejně snadné jako mapování vlastností. Hibernate vyžaduje z důvodu polymorfismu, aby vlastnosti popisující kolekce byly definovány rozhraními a ne jejich implementacemi.
//chybně definovaná kolekce HashSet<User> managers = new HashSet<User>(); //správně definovaná kolekce Set<User> managers = new HashSet<User>();
Takto definované kolekce jsou běžnou konvencí, která se v programování často používá, nicméně Hibernate to vyžaduje z důvodu hlídání tzv. „špinavých objektů“ , které se v kolekci změnily (dirty checking). Jakmile dojde k zperzistentnění rodičovského objektu, naše kolekce bude nahrazena její Hibernate implementací. Tato implementace je schopná zachytit jakoukoli změnu na objektech uvnitř uchovávaných a tyto změny promítat ihned do databáze. Zde samozřejmě záleží na nastavení Hibernate, jak se k těmto změnám postaví. Manipulace se špinavými objekty není jen záležitostí kolekcí, ale i samotných objektů.
Nezapomeňme minimálně ve všech třídách reprezentujících objekty
uchovávané v kolekcích překrýt metody
hashCode() a
equals() tak, aby reflektovaly aplikační klíč
(viz. podkapitola 3.3.). Bez tohoto zásahu nebudou kolekce ani jejich
mapování fungovat správně.
Pomocí elementu <set> jsme schopni
namapovat kolekci objektů. Mapování objektů do množin je nejběžnější
praxí. Tato kolekce bude implementovat rozhraní
Set. Výše zmiňované správce jednotlivých
soutěží namapujeme následovně ve
SportEvent.hbm.xml.
<set name="managers" table="USER_MANAGE_SPORTEVENT"
cascade="save-update" lazy="true">
<key column="SPORTEVENT_ID"/>
<many-to-many column="USER_ID" class="User"/>
</set>
Tato definice říká, že objekt typu
SportEvent obsahuje množinu správců dané
soutěže (managers). Tito správci jsou objekty
typu User. Na úrovni relační databáze mluvíme
o vazbě mnoho na mnoho v podobě asociativní tabulky
USER_MANAGE_SPORTEVENT, která odkazuje na
záznamy do tabulek USER a
SPORTEVENT.
Pomocí tohoto mapování nejsme však schopni do asociativní tabulky doplnit žádné další parametry. Pokud bychom chtěli evidovat např. datum, kdy začala být daná soutěž spravována daným správcem, potom bychom museli použít mapování jiné, s další třídou namapovanou na asociativní tabulku, která by odkazovala jak na správce tak na soutěž a navíc by obsahovala vlastnost typu Date, reprezentující datum zahájení správy soutěže.
Na kolekci je definován atribut cascade s hodnotou save-update, která
říká, že pokud do této kolekce vložíme nového správce a objekt typu
SportEvent necháme pomocí Hibernate sezení
uložit, tato akce se bude propagovat až na úroveň objektů typu
User a tento nový uživatel bude uložen také.
Pokud v této kolekci upravíme vlastnosti nějakého správce a objekt
typu SportEvent aktualizujeme (update) pomocí
Hibernate sezení, změna tohoto správce se také projeví v databázi.
Naproti tomu smazání dané sportovní události nebude mít na žádného
uživatele z této kolekce vliv. Tomuto chování říkáme
tranzitivní perzistence.
Dalším velmi zajímavým a důležitým atributem je atribut lazy, se kterým se blíže seznámíme v podkapitole 3.3.2.
Element <key> v tomto případě ukazuje
na cizí klíč SPORTEVENT_ID asociativní tabulky
USER_MANAGE_SPORTEVENT.
Aby toto mapování fungovalo, je třeba ješte nadefinovat druhý
konec této relace. To uskutečníme v
User.hbm.xml. Tato relace je obousměrná, tudíž
musíme namapovat také kolekci sportovních událostí na objekt typu
User.
<set name="sportEvents" table="USER_MANAGE_SPORTEVENT"
inverse="true" lazy="true">
<key column="USER_ID"/>
<many-to-many column="SPORTEVENT_ID" class="SportEvent"/>
</set>
Toto mapování přináší do hry další atribut a tím je atribut inverse. Pokud je asociace obousměrná, jedna strana musí být označena atributem inverse s hodnotou true. Tato strana potom nedisponuje vymožeností typu tranzitivní perzistence. V našem případě se tedy jakákoli manipulace s kolekcí sportovních událostí nepromítne do databáze.
Další z kolekcí, kterou můžeme v Hibernate použít je kolekce
implementující rozhraní Map, jak se můžeme
sami přesvědčit v souboru
SportEventResultItem.hbm.xml.
<map name="values" cascade="all-delete-orphan"
lazy="true" inverse="false">
<key column="ITEM_ID"/>
<map-key type="string" column="value_key"/>
<one-to-many class="SportEventResultItemValue"/>
</map>
Tato kolekce obsahuje hodnoty položek výsledné tabulky dané
soutěže pro daného soutěžícího, indexované typem hodnoty. Toto
indexování se provádí na základě elementu
<map-key>, který říká jaký sloupec
asociované tabulky se pro indexování použije a jakého je
typu.
Hibernate samozřejmě dokáže také pracovat s kolekcí typu
List. Toto mapování můžeme najít např. v
souboru PlayingGroup.hbm.xml. Na kolekci typu
List je schopen také namapovat elementy
<bag> a <idbag>.
Popis těchto mapování je nad rámec této práce, ale my jej můžeme
kdykoli najít v oficiální dokumentaci
[12].
Klasickou ukázkou obousměrného mapování je v relačním pojetí
vazba mnoho na jednoho a tu si nyní ukážeme. Blíže se podíváme na
relaci mezi regiony (Region), což je kromě
jiného také klasická asociace typu
„rodič-potomci“.
<set name="subRegions" cascade="all-delete-orphan"
lazy="true" inverse="true">
<key column="SUPREGION_ID" />
<one-to-many class="Region" />
</set>
Jedna strana asociace je reprezentována množinou potomků, tedy v našem případě je zajímavý opět atribut cascade , který nabývá hodnoty all-delete-orphan. Tato hodnota říká, že všechny akce aplikované Hibernate sezením na rodičovský prvek, budou propagovány i na jeho děti s tím, že pokud bude tento rodič smazán, budou smazány i jeho děti, ovšem pouze v případě, že nejsou součástí jiné kolekce.
Rodičovský prvek je v tomto případě reprezentován mapováním many-to-one, jak je ukázáno na následujícím příkladě.
<many-to-one name="superRegion"
class="Region"
column="SUPREGION_ID"/>
Pro správnou funkci tohoto mapování musíme mít jednu stranu asociace označenou atributem inverse s hodnotou true, ale o tom jsme si říkali již dříve.
Hibernate obsahuje ještě nesčetně druhů různých exotických mapování, které v běžné aplikaci většinou nebudete potřebovat. Tato mapování se nám do našeho textu již bohužel nevešla, ale můžete je samozřejmě najít ve specifikaci k tomuto výbornému, leč někdy složitému nástroji [12].