Práce s perzistentními objekty

V předchozí podkapitole jsme se zmínili o perzistentních třídách. Pokud mluvíme o jejich objektech nebo kolekcích těchto objektů ve spojení s Hibernate, je někdy nutné tyto objekty rozlišovat na základě jejich stavu. Objekty se mohou nacházet v těchto třech stavech.

Dočasné objekty

Za dočasné objekty (Transient Instances) považujeme objekty, které nikdy nebyly v relaci s Hibernate sezením a tudíž nemají perzistentní identitu a odpovídající řádek v databázi. Tyto objekty nemají nastavený primární klíč. Jedná se zejména o nově vytvořené objekty.

Odstavené objekty

Za odstavené objekty (Detached Instances) považujeme ty objekty, které mají svoji perzistentní identitu a pravděpodobně i odpovídající řádek v databázi, ale právě nejsou v oblasti působnosti Hibernate sezení. Jedná se především o objekty, které byly vybrány z databáze a nyní se používají v aplikaci mimo Hibernate sezení. Hibernate neručí za to, že změna těchto objektů bude reflektována v databázi.

Perzistentní objekty

Objekty, které mají svoji perzistentní identitu a jsou v oblasti působnosti Hibernate sezení. Každá změna provedená v těchto objektech je prováděna také nad databází (samozřejmě na základě zvolené strategie ukládání dat z paměti na disk, tzv. flushování).

Jelikož naše třídy chceme používat také v kolekcích a budeme chtít využívat mechanismu znovuasociování odstavených objektů s jiným Hibernate sezením, musíme překrýt metody equals() a hashCode() tak, aby vyhovovaly našim potřebám. Vlastnosti porovnávané v těchto metodách by měly jednoznačně identifikovat objekt mezi ostatními, což znamená, že by měly reflektovat tzv. aplikační klíč (bussiness key). Aplikační klíč je tvořen těmi vlastnostmi, které jednoznačně identifikují daný objekt v reálném světě. Poznamenejme, že pro tento druh klíče jsou vhodné zejména neměnné a jedinečné vlastnosti.

Aplikační rámec Spring nám poskytuje pro manipulaci s perzistentními objekty, Hibernate sezením a továrnou sezení podpůrné třídy. Jedná se o třídy HibernateTemplate a HibernateDaoSupport .

Práce s perzistentními objekty prostřednictvím HibernateTemplate

Nejdříve se zmiňme o třídě HibernateDaoSupport. Tato třída v sobě drží asociovanou továrnu sezení a poskytuje přístup k HibernateTemplate, což velmi usnadňuje práci s datovou vrstvou. Třídy naší datové vrstvy musí rozšiřovat tuto podpůrnou třídu následujícím způsobem a díky tomu získají přístup k továrně sezení. Ukázku čerpáme z třídy CompetitorDaoHibernateImpl.

public class CompetitorDaoHibernateImpl 
       extends HibernateDaoSupport 
       implements CompetitorDao {

  ....

}

Aby vše fungovalo správně, potřebujeme ještě nastavit této perzistentní třídě odkaz na továrnu sezení. To uděláme pomocí konfigurace v souboru applicationContext-hibernate.xml.

<bean id="competitorDao" 
      class="CompetitorDaoHibernateImpl" >
      <property name="sessionFactory" ref="sessionFactory"/>
</bean>

Jak vytvořit a nakonfigurovat továrnu sezení jsme si ukázali již dříve. Jelikož jsme vše nakonfigurovali správně, máme nyní v této třídě přístup k Hibernate sezení a můžeme začít manipulovat s perzistentními objekty.

Aplikační rámec Spring poskytuje pomocí HibernateTemplate přístup ke všem metodám pro manipulaci s objekty, které Hibernate nabízí. Proč tedy používáme tyto metody a ne metody poskytované přímo Hibernate? Je to z důvodu toho, že tyto podpůrné metody za nás řeší mnoho dalších věcí. Vše si vysvětlíme na následujícím příkladu ze třídy UserDaoImpl.

public List getUsers() {
  return getHibernateTemplate().find("from User user");
}

Metoda získá všechny uživatele systému. Díky použití HibernateTemplate se nemusíme starat o korektní otevření a uzavření Hibernate sezení, o zachytávání výjimek ani o správu transakcí. Tohle vše za nás udělá aplikační rámec Spring. HibernateTemplate nabízí podobnou podporu i pro další metody, jakými jsou save(), update(), load(), merge(), delete() atd.

Pro složitější dotazy nám Spring poskytuje následující konstrukci, jak se můžeme přesvědčit ve třídě UserDaoImpl. Pokud použijeme tuto konstrukci zpětného volání, musíme implementovat metodu doInHibernate(), která nám umožňuje přístup k Hibernate sezení. Toto sezení je pod správou transakčního manažera, tudíž všechny příkazy zde provedené jsou v transakci.

public int getNumberOfUsers() {
 Integer count = (Integer) getHibernateTemplate().execute(
  new HibernateCallback() {
    public Object doInHibernate(Session session)
       throws HibernateException, SQLException {
       Query query = session.createQuery("SELECT count(*) FROM User user");
       return query.uniqueResult();
    };
  });
 return count.intValue();
}

Právě jsme si představili podporu Springu pro manipulaci s perzistentními objekty. Nyní se podíváme na jednotlivé metody podrobněji.

Ukládání objektu

Uložení objektu, který jsme právě vytvořili nebo ho Hibernate považuje za objekt dočasný, není nic složitého, jak si můžeme ukázat na metodě saveUser() ze třídy UserDaoImpl.

public List saveUser(User user) {
  return getHibernateTemplate().save(user);
}

Hibernate otestuje, jedná-li se opravdu o dočasný objekt a uloží ho. Pokud tento objekt získává generovaný identifikátor, tento identifikátor mu je přidělen. Pokud objekt vlastní další asociované objekty, Hibernate je uloží též pod podmínkou, že v mapování je tranzitivní perzistence povolena. Pokud Hibernate vyhodnotí, že se nejedná o dočasný objekt, vyhodí výjimku.

Mazání objektu

Mazání objektu je opět velmi jednoduché.

public void removeUser(User user) {
  getHibernateTemplate().delete(user);
}

Aktualizace objektu

Klasickou prací s daty je jejich aktualizace. Data uložená v databázi vybereme, v uživatelském rozhraní upravíme a uložíme zpět. Pokud pracujeme v prostředí webové aplikace, objekty, které upravujeme v uživatelském rozhraní, jsou objekty odstavené. Hibernate nám však poskytuje možnosti jejich opětovného asociování s Hibernate sezením a uložení jejich modifikovaného stavu. Tohoto dosáhneme pomocí metod update(), saveOrUpdate() a merge().

public void updateUser(User user) {
  //getHibernateTemplate().saveOrUpdate(user);
  //getHibernateTemplate().merge(user);
  getHibernateTemplate().update(user);
}

Jaký je rozdíl mezi těmito metodami? Metodu update() používejme v případě, že s Hibernate sezením není asociovaný ten stejný objekt (se stejným identifikátorem), který ukládáme. Pokud tento objekt existuje v Hibernate sezení, použijme metodu merge(). Metoda saveOrUpdate() dočasný objekt zperzistentní a objekt odstavený aktualizuje. Jinak se chová jako metoda update(). Samozřejmě i zde platí tranzitivní perzistence.

Hibernate za podpory rámce Spring nám poskytuje následující možnosti výběru dat z databáze v podobě objektů.

Získávání objektů

Data z databáze nám Hibernate umožňuje získávat několika způsoby. Základní způsoby jsou tři. Data můžeme získávat pomocí metod get()/load(), dotazovacího jazyka HQL (Hibernate Query Language) a tzv. kritérií. Hibernate dále umí získávat objekty na základě klasického SQL a na základě dotazů podobných zadanému příkladu (Query By Example - QBE). QBE je vhodné zejména pro získávání objektů na základě kritérií zadávaných uživateli do vyhledávacího formuláře.

Získávání jednotlivých objektů

Předtím, než se vrhneme na dotazovací jazyk HQL, si ještě představme metody load() a get(), které jsou vhodné pro výběr jednoho objektu z databáze.

public Competitor getCompetitorById(String id) {
  return (Competitor) getHibernateTemplate().get(Competitor.class, id);
}

Rozdíl mezi metodou get() a load() je následující. Metoda load() vyhodí výjimku v případě, že záznam v databázi neexistuje. Metoda get() vrací ve stejném případě null. Pokud je záznam nalezen, obě metody ho získají, ovšem metoda get() pouze ve formě neinicializovaného proxy objektu.

Získávání objektů na základě dotazovacího jazyka HQL

Dotazovací jazyk HQL je objektovou náhražkou známého dotazovacího jazyka SQL, podporující dědičnost, polymorfismus a asociace. Hibernate poskytuje pro zpracování výrazu, který HQL představuje, dotazovací API. Tento výraz můžeme reprezentovat pomocí implementace rozhraní Query , které krom vykonání totoho výrazu dále poskytuje metody pro vkládání parametrů a manipulaci s výsledkem dotazu (ResultSet).

Aplikační rámec Spring nám potom poskytuje podporu pro zpracování instance třídy Query pomocí zpětného volání v podobě implementace rozhraní HibernateCallback, jak jsme si ukázali v podkapitole 3.1.

HQL je mocný aparát a má téměř shodnou vyjadřovací schopnost jako SQL. Nyní si ukážeme několik běžných příkladů, na kterých si tuto schopnost ilustrujeme, a současně si také ukážeme některé možnosti rozhraní Query. Všechny tyto příklady jsou v plné verzi dostupné v souboru UserDaoImpl.java, který ilustruje, jak správně používat Query API v prostředí rámce Spring.

Následujícím HQL dotazem vybereme všechny uživatele.

Query usersQuery = session.createQuery("from User user");
List<User> users = usersQuery.list();

Část klauzule SELECT je v tomto případě nepovinná.

Na následujícím příkladu si ukážeme nahrazování pojmenovaných parametrů v dotazech. Hodnoty předávané v parametrech budou ošetřené proti výskytu speciálních znaků. Hibernate samozřejmě vnitřně používá klasický JDBC PreparedStatement.

Query query = session.createQuery("FROM User user WHERE username = :username");
query.setString("username", username);
User user = (User)query.uniqueResult();

Pokud chceme vybrat všechny uživatele na základě jejich role, můžeme to provést následujícím způsobem.

String hql = "SELECT user " +
             "  FROM User user, Authority authority " +
             " WHERE authority.name = :role " + 
             "   AND authority in elements(user.authoritySet)";
Query query = session.createQuery(hql);
query.setString("role", role);
List<User> users = (List<User>)query.list();

Rámec Spring nám také poskytuje pomocnou metodu pro získávání objektů přímo na základě HQL. Je jí metoda find() třídy getHibernateTemplate. I tato metoda umí parametrizované dotazy.

String hql = "from User where registrationKey = ?";
List users = getHibernateTemplate().find(hql, key);

Možnosti jazyka HQL jsou natolik rozsáhlé, že je zde nejsme chopni všechny zachytit. Oficiální dokumentace [12] je však velmi kvalitní a možnosti HQL velmi dobře popisuje.

Získávání objektů na základě kritérií

Hibernate poskytuje API, které nám umožňuje stavět dotazy pomocí manipulace s objekty za běhu bez přímé editace řetězců, tak jak tomu bylo např. u HQL. Pokud naše dotazy vyjádříme pomocí kritérií, tyto dotazy budou lépe čitelné, samodokumentující a jejich syntaxe bude parsovatelná a ověřitelná při kompilaci, ale nic není bez chyby. Pokud toto API použijeme, ztratíme část vyjadřovací schopnosti a síly dotazů HQL.

Dotazování na základě kritérií je reprezentováno rozhraním Criteria. Toto rozhraní reprezentuje strom objektů kritérií (Criterion), které jsou na daný dotaz aplikovány. Rozhraní Restrictions poskytuje statické tovární metody pro vytváření objektů kritérií.

V našich příkladech budeme používat pro reprezentaci stromu kritérií rozhraní DetachedCriteria. Toto rozhraní nám umožňuje sestavovat dotazy mimo Hibernate sezení na odstavených objektech a jeho následné zpracování pomocí metody rámce Spring findByCriteria(). Pokud bychom chtěli použít klasické rozhraní Criteria, museli bychom používat a implementovat zpětné volání rozhraní HibernateCallback, poskytované Springem pro získání Hibernate sezení (viz. třída SportBranchDaoImpl). Jiný rozdíl mezi Criteria a DetachedCriteria není.

Na příkladu výběru regionu ze třídy RegionDaoHibernateImpl si uvedeme použití tohoto API.

public Region getRegion(int level, String codeName) {

 DetachedCriteria criteria = DetachedCriteria.forClass(Region.class);
 criteria.add(Restrictions.eq("level", level))
         .add(Restrictions.eq("codeName", codeName));

 List<Region> regions = (List<Region>)getHibernateTemplate()
                                 .findByCriteria(criteria, 0, 1);

 if (regions.isEmpty()) {
   return null;
 }
 
 return regions.get(0);
}

Jak si můžeme všimnout na ukázce, nejdříve vytvoříme objekt reprezentující strom kritérií (požadavků) dotazu pro danou třídu. Na tento dotaz klademe pomocí továrních metod poskytovaných objektem implementující rozhraní Restrictions omezení. Následně tento dotaz zpracujeme s tím, že má být vybrán právě jeden region, což nám ale nevadí, protože vlastnost codeName je u každého regionu unikátní.

Pomocí kritérií lze udělat velmi kvalitní a čitelné dotazy všeho druhu. V naší aplikaci můžeme najít ukázky dalšího použití dotazů na základě kritérií. Celý přehled možností tohoto API však najdeme pouze v kvalitní dokumentaci [12] nástroje Hibernate.

Strategie výběru objektů

Jeden z největších problémů obecně u ORM nástrojů jako je Hibernate spočívá v efektivním přístupu k relačním datům. Získávání objektů na základě mnoha přístupů do databáze je v současných mnohouživatelských prostředích nepřípustné. Zjistilo se, že nejefektivnějším přístupem je získávat data v podobě objektového grafu. Efektnost výběru dat v podobě objektového grafu potom můžeme řídit my pomocí nastavování vlastností na asociacích v metadatech mapování objektů na tabulky relační databáze nebo na základě výběru vhodné strategie zpracování dotazů přímo za běhu aplikace.

Výběr objektů se řídí tzv. strategií výběru. Hibernate poskytuje čtyři strategie výběru asociovaných objektů, jmenovitě strategii hladovou (Eager fetching strategy), línou (Lazy fetching Strategy), dávkovou (Batch fetching strategy) a v poslední řadě strategii okamžitou (Immediate fetching strategy).

Okamžitá strategie

Asociované objekty jsou vybrány okamžitě pomocí sekvenčního čtení databáze nebo z keše.

Hladová strategie

Asociované objekty a kolekce jsou vybrány společně s rodičovským objektem pomocí SQL vnějšího spojení (outer join) a žádné další přístupy do databáze nejsou třeba.

Líná strategie

Asociované objekty a kolekce jsou vybírány líně až při prvním přístupu k nim. Tento přístup zapříčiní další dotaz nad databází, pokud hledaná data nejsou v keši.

Dávková strategie

Tato strategie dokáže v některých případech zvýšit výkon líné strategie tím, že získá najednou sadu objektů nebo kolekcí v případě, že je líné získávání dat aktivováno.

Líná strategie je doporučována jako vhodná a efektivní strategie výběru objektů pro většinu aplikací. Pokud bychom chtěli, aby naše aplikace byly co nejefektivnější, museli bychom se zamyslet nad každým jednotlivým případem asociace objektů a kolekcí a explicitně pro tyto jednotlivé případy tuto strategii změnit.

V naší aplikaci používáme hladovou a línou strategii výběru objektů a na příkladech si nyní ukážeme jak.

Hladová strategie

Hladovou strategii aktivujeme na kolekcích nebo asociacích explicitně tak, že na nich v mapování uvedeme atributy outer-join="true" a lazy="false". Tato kolekce poté bude vybrána společně s objektem, který ji vlastní jedním přístupem do databáze pomocí vnějšího spojení.

<set name="authoritySet" table="USER_AUTHORITY" 
     lazy="false" outer-join="true">
 <key column="USER_ID" />
 <many-to-many column="AUTHORITY_ID" class="Authority" />
</set>

Nyní budou uživatelské role vybrány společně s daným uživatelem, viz. User.hbm.xml.

Za běhu aplikace můžeme hladovou strategii aktivovat jak v dotazech pomocí Query API, tak v dotazech na základě kritérií.

public List<GuidePostCategory> getGuidePostCategories() {
  DetachedCriteria criteria = 
      DetachedCriteria.forClass(GuidePostCategory.class);
  criteria.setFetchMode("signPosts", FetchMode.JOIN);

  List<GuidePostCategory> categories = 
         (List<GuidePostCategory>)
         getHibernateTemplate().findByCriteria(criteria);
  return categories;
}

Při zpracování dohoto dotazu bude vždy vybrána kolekce ukazatelů (SignPost) společně s kategorií, do které patří (viz. GuidePostCategoryDaoImpl.java).

Líná strategie

Pro většinu asociací a kolekcí v naší aplikaci však máme nastavenou línou strategii. Líná strategie pracuje na následujícím principu. Pokud chceme vybrat nějaký objekt, nemusíme okamžitě vybrat celý graf asociovaných objektů, nýbrž jsme schopni vybrat pouze tento objekt a asociované objekty a kolekce vybrat až při prvním přístupu k nim. Ve skutečnosti jsou všechny líně asociované objekty a kolekce vráceny ve formě neinicializovaných proxy objektů. Tyto proxy objekty jsou méně paměťově náročné, než objekty plné dat, což je nespornou výhodou této strategie. Velikost grafu asociovaných objektů vybraných při získávání našeho objektu se dá nastavit. Dobrou praxí bývá nastavit všechny kolekce na líné vyhodnocování, což je v Hibernate defaultní nastavení a za běhu toto nastavení případně měnit na jiný druh strategie.

<set name="competitors" table="USER_MANAGE_COMPETITOR" 
     inverse="true" lazy="true">
  <key column="USER_ID"/>
  <many-to-many column="COMPETITOR_ID" class="Competitor"/>
</set>

Na ukázce z User.hbm.xml vidíme klasickou deklaraci kolekce, obsahující všechny soutěžící, které daný uživatel systému spravuje. Na této kolekci je nastaveno líné vyhodnocování pomocí atributu lazy="true". Pokud tedy nyní z databáze vybereme uživatele, kolekce soutěžících, které spravuje nebude vybrána. Vybere se až v momentě zavolání metody getCompetitors().

V naší aplikaci, tak jak je nyní nastavená, bychom této funkcionality mohli využít pouze v datové vrstvě. Líné získávání objektů pracuje s otevřeným Hibernate sezením a to je prozatím dostupné pouze v této vrstvě. Tato vlastnost použití líné strategie ve webových aplikacích založených na třívrstvé architektuře velmi limituje. Aplikační rámec Spring však poskytuje API, které umožňuje Hibernate sezení uržovat aktivní po celou dobu HTTP požadavku. Toto sezení je poté dostupné jak v prezentační, tak i aplikační vrstvě, což je přesně to, co požadujeme.

Rámec Spring poskytuje třídy OpenSessionInViewFilter a OpenSessionInViewInterceptor, které se starají o otevření Hibernate sezení na začátku uživatelského požadavku a na jeho konci jej ukončují. To nám umožní používat líný výběr objektů ve všech vrstvách naší aplikace.

OpenSessionInViewFilter nastavíme ve web.xml následovně.

<filter>
  <filter-name>hibernateFilter</filter-name>
  <filter-class>
   org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
 </filter-class>
</filter>

<filter-mapping>
  <filter-name>hibernateFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Tento filtr je tedy aplikován na všechny požadavky jdoucí na naši aplikaci. Filtr má stejnou funkcionalitu jako interceptor definovaný a popsaný níže.

OpenSessionInViewInterceptor nastavíme v souboru aplicationContext-bussines.xml. Rámec Spring na základě zpracování tohoto interceptoru vytvoří Hibernate sezení a uloží ho do vlákna zpracovávajícího příchozí požadavek, kde je dále k dispozici. Tento postup je nejen vhodný pro netransakční zpracování ale také pro transakce, probíhající nad aplikační vrstvou skrze Hibernate transakčního manažera.

<bean name="openSessionInViewInterceptor"  
class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
  <property name="sessionFactory">
    <ref bean="sessionFactory"/>
  </property>
  <property name="singleSession">
    <value>false</value>
  </property>
</bean>

Konfigurace tohoto interceptoru se řídí několika parametry. Nejdříve ho napojíme na továrnu sezení, která se stará o vytváření Hibernate sezení. Parametr singleSession=false říká, že pro každý přístup k datům nebo pro každou transakci bude vytvořeno nové Hibernate sezení. Nebude tedy použito jedno sezení pro celé zpracování uživatelského požadavku. Každé z vytvořených sezení však bude otevřeno až do konce požadavku.

Tento interceptor poté nasadíme na každý uživatelský požadavek podobně, jako jej nasadíme v naší ukázce na všechny požadavky jdoucí na administrační sekci, viz. applicationContext-admin.xml.

<bean id="urlMappingForAdmin" 
 class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">   
    <property name="order" value="3" />
    <property name="interceptors">
       <list>
        <ref bean="openSessionInViewInterceptor"/>
        <ref bean="requestInterceptor"/>
    </property>
.....
</bean>

Nyní máme vše připravené k tomu, abychom línou strategii získávání objektů mohli používat v praxi.

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.