10. prosince 2008

Překlad výjímek z databáze při použití Hibernate

Kdo používá Hibernate, ten možná již tuší, kam budu dnešním příspěvkem mířit. Jde o to, že chyby, které detekuje Hibernate se snaží do výjimky dávat jméno třídy a atributu, který chybu vyvolal. Ovšem pokud vám chybu vyhodí databáze, pak vás Hibernate nechá ve štichu a předhodí vám výjimku datábáze a dělejte si s ní co chcete.

Toto chování se mi nikdy nelíbilo, protože mu moc nerozumím, Hibernate má vše co potřebuje. Zná dialekt, který by umožnil překlad databázově závislých výjimek, do obecné formy obsahující jméno tabulky a názvy sloupců. Následně je možné využít mapování a přeložit název tabulky a názvy sloupců na jméno třídy a atributů. Proč to Hibernate nedělá? Nevím ...

My jsme vždy chtěli mít vlastní DAO výjimky, které by byly nezávislé na použitém ORM. A zároveň jsme chtěli aby obsahovaly jméno třídy a atributů. Nikoliv tabulky a sloupce, protože tyto výjimky jsou většinou zpracovávány vrstvami aplikace, které o databázi nemají mít nejmenší tušení.

Řešení jsme měli, ale nelíbilo se nám, protože jsme de facto museli mapování zapisovat dvakrát (jednou z objektů do relací pro Hibernate a podruhé z relací do objektů). Dodnes nevím proč nejsou programátorsky přístupné mapovací soubory, nebo nevím jak je z Hibernate dostat, což jsme chtěli pro překlad sloupců na atributy použít. Občas jsme diskutovali co s tím, ale ne a ne přijít na ten správný nápad. Až takhle jednou jdeme z oběda a s kolegou Lubošem jsme si dali brain storming a triviální řešení jsme našli (nechápu jak to, že nás nenapadlo dřív).

Je až trapně triviální. Stačí si rozparsovat Hibernate mapovací soubory a vytvořit z nich mapování opačné, než jaké reprezentují. Tato akce představuje nesložitý algoritmus, jedinou složitostí je dědičnost, kde je nutno vytvořit vhodnou reprezentaci, aby se při zpětném překladu zvolilo správné jméno třídy (to nejkonkrétnější), v případě překladu více atributů umístěných různě hluboko v hierarchii tříd.

Druhým krokem je vytažení informace z SQLException, kterou vyhodí databáze. Zde je pár drobných oříšků, např. získání jmen sloupců v případě chyby unikátního klíče (námi použitá DB2 vrátí jméno unikátního klíče, které je nutno pomocí katalogu překlopit na jména sloupců).

Takže cca. po 3 dnech programování (nejprve jsem si dělal prototyp v groovy a pak jsem vytvořil kompletní implementaci v javě) máme řešení, se kterým jsme maximálně spokojeni. Stačí nám zapisovat Hibernate mapování a kód za nás udělá zbytek. My pak už pouze zachytáváme např. DARequiredException, která obsahuje jméno objektu a jeho atributu.

PS. Kód jsem neuváděl, protože není složitý, akorát ho je poměrně dost, takže by se velmi pravděpodobně jednoduchost sdělení hodně zkomplikovala.

11 komentářů:

Vladimír Oraný řekl(a)...

Velmi zajímavý článek! Jelikož je Hibernate open source, proč nezkusíte prosadit váš nápad do samotného Hibernate.

Jinak občas stačí více číst a míň datlovat :)

Java Persistence with Hibernate - oddíl 3.3.5 uvádí dva způsoby jak se dostat na metadata:
1) z objektu konfigurace Configuration - cfg.getClassMapping(Trida.class.getName()) - vrací objekt PersistentClass
2) ze SessionFactory - sf.getClassMetadata(Trida.class) - vrací objekt ClassMetadata

Jira řekl(a)...

No tak s komunitou okolo Hibernate jsem už párkrát něco řešil, a neznám arogantnější lidi. I když jsem jim udělal patch, tak mě poslali někam (bylo to tuším, že saveOrUpdate a delete se chovali různě, pokud identifikátor objektu byl null).

Jinak zajímavá myšlenka s tou PersistentClass, tou jsem jenom nějak prolítl a přehlédl v ní to podstatné.

Aleš Dostál řekl(a)...

Určitě je to potřebná věc.

Mě by zajímalo, jak postupujete v případě, kdy je hibernate přes JTA.

Osobně používám Hibernate s JPA a EJB3. Problém vyjímek je v tom, že v případě jakéhokoli zachycování je nutné jedině provést flush(); na konci metody. V opačném případě dostanu EJBRollbackException a z té exception z Hibernate či z JDBC nezískám. Jenže onen flush poruší logiku distribuovaných transakcí. Jediné řešení jsem nalezl přes XA společně s definicí auto_flush pro Hibernate. To je ovšem kombinace, která ne vždy spolehlivě funguje. Alespoň na různých verzích MySQL.

Jira řekl(a)...

Nevím co myslíte přesně pod JTA, ale nepoužíváme EJB. Dokonce ani nemůžeme uvolňovat Connection od transakce po provedení příkazu, protože si přes Connection předáváme do DB jméno přihlášeného uživatele (z důvodu logování, kdo prováděl změny), takže Connection je svázána s transakcí po celou dobu běhu transakce.

EJB je dle mého soudu špatně a naštěstí s nimi nemusím pracovat.

Pokud nastane exception, pak záleží na tom co to je zač, zda je nutné vše zahazovat a nebo ne .. a to záleží na konkrétním případě.

Naše řešení je založené na přístupu k SQLException, ze které zjistím co se děje.

Anonymní řekl(a)...

Tak to jsem rad ze pouzivam OpenJPA, to hazi exception mnohem kultivovaneji. A vyvojari nejsou arogantni * :-)

Anonymní řekl(a)...

Co s nimi potom delate, kdyz je takhle chytate? :-)

Resp. na co potrebujete ty informace z metadat kdyz tu vyjimku zpracovavate?

Jira řekl(a)...

Tak např. je chytá UI vrstva a říká uživateli, že políčko je nutné vyplnit, nebo, že kombinace políček není unikátní. Pokud již tam máme jméno třídy a atributů, tak se to uděje automaticky, bez našeho zásahu.

Vladimír Oraný řekl(a)...

nebylo by pro tento případ vhodné spíš využití metadat, případně hibernate validation, pro validaci než až pro ošetřování vyjímek?

Jira řekl(a)...

Na některé věci je možné použít (a vyřešit je) Hibernate Validation. Ale co unikátnost.

Vladimír Oraný řekl(a)...

pro unikátnost, jejíž porušení je doopravdy spíše výjimečný stav se zdá odchytávání výjimek jako správná cesta. anotace pro validaci unikátnosti v Hiberante Annotation doopravdy chybí, ale při troše snahy by šla dodělat přes vlastní validátor s pomocí metadat z PersistentClass (myslím, že po chvilce Googlení byste možná už i našel pár fungujících implementací).

Jira řekl(a)...

No na to abych zjistil, zda je nebo neni neco unikatni muzim udelat vylet do DB a zjistit, zda to tam uz neni. Obavam se, ze bud nezmam Hibernate Annotation, a nebo si nerozumime. Domnival jsem se, ze Hibernate Annotation dokaze anotovat fieldy tak, aby se pred cestou do DB dokazalo overit, zda je objekt v poradku a ma cenu s nim do DB bezet. Unikatnost mi to zaridit nemuze.