Sposób na JPA i LazyInitializationException

Wpis dodany 17 sierpnia 2009 o godz. 22:37:30 w kategorii Hibernate, Java, JPA, Spring, Techblog.

Temat lazy loadingu (leniwego ładowania?) przewijał się ostatnio na polskich blogach javowych. Pisał o nim Mateusz Zięba, o wydajności takiego rozwiązania wspominał też w świetnym wpisie Sławek Sobótka. Czy można coś jeszcze dodać? Myślę, że niewiele, ale spróbujmy skupić się na jednej rzeczy, a mianowicie na zamkniętej sesji JPA i sposobie, jak sobie w takim wypadku radzić gdy mamy jeszcze w naszych zależnościach Spring Framework oraz działamy w środowisku webowym.

Problem jest dość częsty (przykładowy "stack trace" poniżej) i jako taki ma też rozwiązanie, opisane zresztą w artykule Open Session in View na stronach naszego ulubionego ORM-a. W skrócie całe zamieszanie związane jest z tym, że nie zawsze w naszej warstwie logiki biznesowej operujemy na obiektach, które potem chcemy wyświetlić, a zatem ORM bardzo słusznie nie pobiera ich z bazy danych. Jednak gdy dochodzi do wyświetlenia listy w widoku, zamiast np. listy ulubionych kolorów użytkownika dostajemy błąd numer 500, a w logach straszy nas LazyInitializationException (oczywiście w przypadku H., bo specyfikacja JPA o takiej sytuacji w ogóle nie wspomina). Sesja jest już bowiem zamknięta i nie wiadomo, skąd te kolory wziąć, a całej bazy przecież nie pociągnęliśmy wcześniej, prawda? :). Z przyczyn oczywistych zmiana fetch na FetchType.EAGER raczej nie wchodzi w grę.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: net.miracki.Test.categories, no session or session was closed
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException (AbstractPersistentCollection.java:380)
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected (AbstractPersistentCollection.java:372)
    at org.hibernate.collection.AbstractPersistentCollection.readSize (AbstractPersistentCollection.java:119)
    at org.hibernate.collection.PersistentSet.size (PersistentSet.java:162)
    ...

Problem jest, rozwiązanie dla Hibernate'a też. Niestety niewielki kłopot pojawia się, kiedy chcemy być standardowi i używamy Hibernate (albo czegokolwiek innego) poprzez właśnie JPA. Jak to zazwyczaj bywa, Spring Framework ma na takie zachowanie gotową i prostą receptę. W zasadzie wpis mógłby się kończyć na poniższym kawałku pliku web.xml:

    
        JpaFilter
        org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
    
    
        JpaFilter
        /*
    

Dzięki temu filtrowi nie powinniśmy więcej spotkać wyjątku związanego z "lazy loadingiem". Powyższy filtr istnieje zresztą też w wersji dla zwykłego Hibernate (jako klasa OpenSessionInViewFilter).

Abstrahując już od głównego tematu, ORM-y to narzędzia, które przed użyciem wypada przynajmniej choć trochę poznać, a do tego tutorial jak zrobić "Hello World" nie wystarczy. Brałem udział w projekcie, który z dobrodziejstw opisanych filtrów nie korzystał, przez co niektóre metody w warstwie logiki biznesowej posiadały argumenty typu boolean o nazwach attachCategories, attachCountries itp. Nie wiem, czy byłbym jednak w stanie wymienić wszystkie zasady dobrego kodowania, które w ten sposób zostały złamane ;). Ale jeśli chodzi o ORM-y, to prawie na samej górze mojej prywatnej listy idiotycznych rozwiązań jest adnotacja @Transient dla obiektów połączona z mapowaniem kluczy obcych do Longów z @Column, bo "pociągniesz całą bazę, ORM-y to zło, ale niech już zostaną". Na całe szczęście takie podejście bardzo szybko minęło wraz z lekturą manuala...

Komentarze
Hoppke napisał(a) dnia 18 sierpnia 2009 o godz. 00:48:00:

Święta prawda.

Inna sprawa, że atrybuty "attachFoo" i tak nie są IMO tak brzydkie, jak "długie" sesje (czy też "session in view"), bo chyba mniej rzeczy można skopać/trudniej strzelić sobie w stopę.

Lepiej owinąć widok w sesję niż np. wymusić eager prawie wszędzie (widziałem kiedyś taką patologię, strach było czegokolwiek dotknąć, bo albo zaczynało przy byle okazji pół bazy wyciągać, albo się wysypywało na braku dostępu do sesji)

...lecz wg. wielu ideałem jest pociągnięcie z bazy wszystkiego, co potrzebne do wyrenderowania widoku PRZED przekazaniem pałeczki dalej (czyli NIE udostępniamy warstwie widoków obiektów zwróconych bezpośrednio przez ORM -- bardzo zdrowa zasada, IMO). Często spotykane rozwiąznie to np. marshalling do XML-a (czy innej postaci) i dopiero potem renderowanie tego formatu pośredniego (ma to dodatkowe minusy, ale i sporo dodatkowych plusów).

Kiedyś (głównie w środowiskach "enterprisey") dość popularne było robienie kilku (na ogół dwóch) wersji ciężkich/kłopotliwych obiektów. Np. Document (z masą pól/dociąganych "gorliwie" relacji) i DocumentLite, który zawierał tylko niektóre dane "pełnego" dokumentu. Chyba nawet uchodziło to za jakiś wzorzec :) Na szczęście od dawna nie widziałem, by ktoś tak kombinował. Pewnie w jakimś drobnym odsetku problemów da się takie podejście wybronić...

A w złożonych przypadkach i tak często trzeba wyskrobać dedykowanego HQL-a, który odpowiednie joiny porobi by wyjąć dane wydajnie (zamiast narzekać jakie to ORM są powolne...)

Koniec końców ORM wydaje się być często świetnym, prawie magicznym narzędziem, ale mimo automatyzacji i abstrakcji (a może właśnie przez nie?) trzeba zawsze poświęcić sporo czasu na zastanowienie się, co będzie najlepsze w danym projekcie....

Antek napisał(a) dnia 18 sierpnia 2009 o godz. 08:08:57:

ORM ma kilka lat historii. Relacyjnym bazom danych broda już zsiwiała. Na razie, aby uniknąć niepotrzebnych utrudnień, musimy nie bać się używać JDBC.
Dzięki za artykuł, będę pamiętał o tym wypasionym filtrze.

Mateusz napisał(a) dnia 19 sierpnia 2009 o godz. 12:11:02:

@Antek
Wielu programistów dzięki ORM zrobiło wielki krok naprzód, tyle że niektórzy z nich stali wtedy nad przepaścią. Co oczywiście nie zmienia faktu, że w większości przypadków ORM-y bardzo ułatwiają życie.

@Hoppke
Nie udostępnianie encji w widoku ma sporo sensu, zwłaszcza jeśli chodzi o kwestie security (kiedyś ogłaszano z wielką pompą o związanym z tym bugu bodajże w Spring MVC). Niestety wiąże się to z produkowaniem sporej ilości DTO, co jest zadaniem nieco odmóżdżającym, choć czasem nadal koniecznym.
A przy (zbyt) automatycznej serializacji encji JPA do xml/json (np. używając liba XStrem) czekają na nas niestety niespodzianki z cglibowymi proxy.

Antek napisał(a) dnia 26 sierpnia 2009 o godz. 10:10:11:

Trochę z innej beczki. Właśnie zauważyłem w JPQL coś takiego jak "join fetch" umożliwiające pobranie potrzebnych nam danych w zapytaniu, bez konieczności deklarowania FetchType.EAGER.

http://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/jpa_overview_query.html#jpa_overview_join_fetch

arek napisał(a) dnia 19 października 2010 o godz. 18:18:56:

Open Session in View to jedyne możliwe rozwiązanie problemu LazyInitializationException w springu ale ma ono spore wady. Transakcje comituje filtr na koncu przetwarzania requestu i po fazie renderowania widoku. Jeżeli commit sie nie uda to mamy problem bo widok mógł juz zostac wyslany do klienta poza tym moga wystapic wyjatki NonUniqueObjectException.
W ksizce "Spring Framework. Profesjonalne tworzenie oprogramowania w Javie" opisany jest ten wzorzec następnie wymieniane są jego wady i pojawia się konkluzja ze wobec tych wad może lepiej dać sobie z lazy load spokój.
Moim zdaniem problem jest naprawdę rozwiązany dopiero w jboss seam.
Seam wiąże sesje hibernetowa z konwersacja która możne obejmować wiele requestow (np wizad)
i nie zamyka sesji hibernetowej po każdym requesicie jak spring.
W ten sposób możemy w pełni skorzystać z usługi wykrywania zmian na grafie obiektów. Na końcu wizadra robimy flash nie musimy robić merga w którym tez są problemy
W fazie renderowania seam otwiera osobna transakcje wiec nie ma problemu jeżeli commit się nie uda.
Tak naprawdę hibernate potrzebuje stanowego frameworka.

Dodaj komentarz
captcha