Mapování perzistentních tříd

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.

Jedna třída mapovaná na jednu tabulku

Nejjednodušší způsob mapování tříd. Nepodporuje dědičnost a polymorfismus.

Hierarchie tříd mapovaná na jednu tabulku

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.

Každá třída hierarchie mapovaná na jednu tabulku

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.

Každá konkrétní třída hierarchie mapovaná na jednu tabulku

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.

Mapování tříd

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.

Atribut name

Název perzistentní třídy včetně balíku, pokud není nastaven atribut package elementu <hibernate-mapping>.

Atribut table

Název tabulky v relační databázi, na kterou se bude naše třída mapovat.

Atribut proxy

Obsahuje rozhraní, které bude objekt implementovat, pokud bude součástí líně vyhodnocovaných kolekcí či objektů.

Atribut where

Dokáže omezit výběr dat reprezentovaných touto třídou nějakou podmínkou.

Atribut lazy

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.

Mapování vlastností tříd

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íč.

Mapování identifikátoru třídy na primární klíč tabulky

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.

Atribut name

Jméno mapované vlastnosti.

Atribut column

Sloupec databázové tabulky, na kterou se bude daná vlastnost mapovat. Pokud není určen, jméno atributu bude shodné s názvem vlastnosti.

Atribut type

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.

Mapování vlastností primitivních typů

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>.

Elementy timestamp a 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"/>

Mapování asociací mezi perzistentními třídami

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é.

Jednosměrná many-to-one asociace

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í.

Asociace one-to-one

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.

Mapování komponent

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>

Mapování podporující polymorfismus a dědičnost

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.

Polymorfní mapování podle strategie hierarchie tříd mapovaných 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.

Polymorfní mapování podle strategie mapování podtřídy na jednu tabulku

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.

Obrázek 6.3. Část databázového schématu pomocí <joined-subclass>

Část databázového schématu pomocí <joined-subclass>

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ě.

Obrázek 6.4. Část databázového schématu pomocí <subclass>

Část databázového schématu pomocí <subclass>

Polymorfní many-to-one asociace

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í

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ě.

Mapování kolekcí typu množina

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.

Mapování kolekcí typu mapa

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.

Mapování kolekcí typu List

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].

Mapování obousměrných asociací

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].



[1] http://www.hibernate.org/hib_docs/v3/reference/en/html_single/

Komentáře

Téma neobsahuje žádné komentáře.

Vložit komentář

Můžete používat značkovací jazyk Texy!


Jméno:
E-mail:
Url:
Komentář:
1 + 2 =
 
MoroSystems, s.r.o.