Wednesday, January 26, 2011

Trapenie s Hibernate proxy

V nasledujucom clanku popisem jeden z pripadov, kedy sa vam moze vytvorit problematicka hibernate proxy.

V podstate to je celkom jednoduche. Ked mate velku stromovu strukturu, nie stale chcete aby sa vam nacitali vsetky objekty, ktore sa v nej nachadzaju. Chcete vsak aby sa niektore casti dotahovali len ak ich treba, pripadne sa dotiahnu v specifickych selectoch (napriklad pouzitim LEFT JOIN FETCH).
Aby sa toho dosiahlo definuje sa na niektore vazby/fieldy lazy loading = FetchType.LAZY (teda sa z databazy nacitaju az ked ich skutocne treba). No a niekedy sa stane, ze niekto toto urobi aj na fieldy ktorych trieda predstavuje hierarchicku strukturu (toto je zakladny kamen urazu). Teda napriklad je takto oznaceny field typu Animal od ktoreho je oddena trieda Cat, Dog, a Mouse.

Ak si teda nacitate triedu, ktora ma lazy loaded field, hibernate pre neho vytvori proxy (hibernate to takto robi pre vsetky lazy loading fieldy). A az ked sa na tej proxine zavola nejaky getter/setter tak hibernate spusti mechanizmus, ktory z databazy dotaha vsetky informacie a tento (doteraz nedotiahnuty) objekt vytvori. Takze teraz mame field ktory odkazuje na proxinu a ta sa dalej odkazuje (v sebe zaobaluje) skutocne nacitany (nami ocakavany objekt). Vsetky dotazy na gettre a settre sa potom cez proxinu zavolaju na tomto objekte.

No a teraz prichadza ta zaujimava cast: dovodom preco sa dava lazy loading je aby sa nerobilo zbytocne vela selectov do tabuliek, ktorych data sa vobec nevyuzivaju. Co znamena, ze sa teda nerobi ani select do tabuliek pre dany lazy loading field. A tu je ta dolezita vec. Hibernate teda v case vytvarania proxy nepozna skutocny typ objektu, ktory pod proxinou bude, nakolko nevykonal selekt ktory by mu povedal, ze to je napriklad Dog. Nakolko teda nevie, ci to bude Dog, Cat, Mouse alebo Animal, vytvori hibernate proxinu ktora ma ten isty typ ako je zadefinovany na fielde. V nasom pripade teda vytvori proxinu typu Animal. A aj ked sa pod nou neskor dotiahne objekt typu Dog, tak ked na tejto proxine urobime instanceof Dog, tak nam java vyhodi false. Dovodom je, ze aj ked proxina pod sebou ma ulozeny objekt typu Dog ona sama je typu Animal (a nas field sa odkazuje na tuto proxinu a nie na konkretny objekt pod nou). Inak len tak pre zaujimavost: v pripade ze by Animal, Dog, Mouse a Cat boli interfacy, potom by proxina implementovala VSETKY (teda instanceof by vratila true na lubovolny z nich).

Takze, uz len jedna vec a mame to hotove :D.

p.s.: tato chyba je viac menej sposobena zlym navrhom
p.s.2: plus toto je specificka vec, ktora sa prikladom neda ukazat, nakolko z prikladu nevidno ze vobec nejaky problem existuje.

Takze, ale aby sme pokracovali dalej. Dufam, ze dovodu, preco hibernate take proxiny vytvara ste teda pochopil?

A teraz prichadza na radu dalsia taka zaujimava vec: hibernate ma jedno klucove pravidlo. V pripade, ze bola entita nacitana do sessiony (stane sa manazovanou) musi byt tato entita (v danej sessione) reprezentovana len jednym jedinym objektom. Co v skratke znamena ze ak by ste v jednej sessione urobil dvakrat za sebou select na jednu a tu istu entitu, dostanete stale identicky objekt. Nie teda objekt s rovnakymi fieldami ale UPLNE TEN ISTY objekt. Teda zmena v objekte z prveho selektu sa premietne v objekte z druheho (nakolko ide o jeden objekt). To vsak znamena i to, ze ked je entity reprezentovana proxinou, tak lubovolny selekt na nu vrati tiez (v danej sessione) proxinu. Co uz vsak trosicku zavana problemom.

Moze sa totiz stat situacia ze, niekto si dotiahne objekt, ktory obsahuje odkaz na danu entitu typu Animal. A nakolko tento odkaz/field je urceny na lazy loading vytvori sa pre danu entitu proxina. Dajme tomu ze toto urobila metoda A. Nasledne sa zavola niekde inde v kode (ale stale v tej istej transakcii/sessione) metoda B. A v metode B (ktoru pisal niekto iny ako metodu A) sa dotazeme na tuto Animal entitu. Hibernate nam vsak vrati proxinu, pod ktoru dotiahne uz konkretny objekt typu Dog (nakolko aj pre metodu A aj pre metodu B islo o tu istu entitu a MUSI to teba byt ten isty objekt). Bohuzial vsak tato proxina je typu Animal a nakolko my potrebujeme upravovat fieldy Dog-a, dostali sme sa do slepej ulicky. Najhorsie vsak je, ze v tomto pripade uz nepomoze ani LEFT JOIN FETCH na dane Animal.

Toto je asi najcastejsie ako sa tato chyba prejavuje (hlavne ak sa na pisani kodu podiela viac ludi). Clovek si dotiahne objekt o ktorom vie ze je urciteho typu ale miesto toho mu pride len proxina typu superclass. A fakt netusi preco tomu tak je. A mozno len nahodou pride na to, ze predtym sa volal nejaky (mozno validacny kod) niekoho ineho, ktory sposobil, ze tato entita je do hibernate sessiony ulozena ako proxy a hibernate pri kazdom dalsom dotaze nic ine ako proxy nevrati.

Hotovo :D

No teda, skoro. Zostava tu totiz este jeden problem. Nakolko s tymto objektom potrebujeme nejako realne robit, tak nam ta proxina trosku vadi. Casto sa takato chyba odhali vo faze projektu, kedy nejaka rapidnejsia upravu designu predstavuje problem a tento problem je teda potrebne riesit nejak inak.

V uvahu pripadaju take tri zakladne riesenia, pricom prve dve z nich su skoro az hnusne:
  1. nacitat a spracovat entitu v novej sessione/transakcii (pre vacsinu pripadov nerealne riesenie)
  2. nacitanu manazovanu entitu vyhodit zo session cache
    • vyhodenim vsetkych objektov zo session cache (trosku drasticke riesenie)
    • nacitat problematicky objekt, vyhodit ho zo session cache a znovu ho nacitat (trosku hlupe riesenie)
  3. pouzit util triedu ktora sa o to postara (mnou preferovana varianta):

public class ProxyUtil {

public static <T> T deproxy(final T object) {
// deproxy object
if (object == null) {
return null;
}
return (object instanceof HibernateProxy) ?
(T) ((HibernateProxy) object).getHibernateLazyInitializer().getImplementation() :
object;
}
}

Tato utilka zoberie ako vstup lubovolny objekt. Ak je tento objekt hibernate proxy, vrati objekt pod nou ulozeny, inak vrati nezmeneny objekt.

Tak a teraz je to uz fakt vsetko