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.
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.
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.
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
.
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.
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 je opět velmi jednoduché.
public void removeUser(User user) {
getHibernateTemplate().delete(user);
}
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ů.
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.
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.
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.
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.
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).
Asociované objekty jsou vybrány okamžitě pomocí sekvenčního čtení databáze nebo z keše.
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.
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.
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.
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).
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.