Entity Manager i okolice – część 2

Dziś będziemy kontynuować zgłębianie tajemnic Entity Managera. Konkretnie zaś zajmiemy się stanem encji – co się z nimi dzieje kiedy powstają i zaczynają być zarządzane, a co kiedy przy próbie zapisu otrzymamy wyjątek, a nasze encje zostaną odłączone (tzw. detach). Do dzieła.

Co się dzieje z moją encją?

Załóżmy, że tworzymy od podstaw bohatera. Kawałeczek kodu dla przypomnienia:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");
		
List<Nickname> nickNames = new ArrayList<Nickname>();
Nickname n1 = new Nickname(); n1.setNick("Ripper");
Nickname n2 = new Nickname(); n2.setNick("Butcher");
Nickname n3 = new Nickname(); n3.setNick("Killer");
nickNames.add(n1);
nickNames.add(n2);
nickNames.add(n3);		
h.setNicknames(nickNames);
		
FinanceState finances = new FinanceState();
finances.setCooper(10);
finances.setSilver(20);
finances.setGold(1);
h.setFinance(finances);
		
Dragon d = new Dragon();
d.setName("Smok Wawelski");				
d.setRider(h);
h.setDragon(d);             
		
entityManager.persist( h );

Po utworzeniu encji stają się one zarządzane (ang. managed ). W tym momencie wszystkie przynależą do peristence context, który jest strukturą rezydującą w pamięci. Wszelakie operacje na encjach są dokonywane w pamięci i dopiero kiedy następuje commit transakcji mamy pewność, że JPA próbuje zapisać zmiany w bazie danych. Proces ten określa się ładnym angielskim słowem – flush. Dokładnie tak samo nazywa się metoda Entity Managera, która robi dokładnie to samo – próbuje wykonać na bazie danych wszystkie operacje, które wykonano w ramach bieżącej transakcji. Zatem jeżeli uważamy, że w danym momencie istnieje konieczność zapisania w bazie zmian dokonanych na encjach możemy wymusić na JPA takie właśnie zachowanie. Oto zmodyfikowany przykład:

Dragon d = new Dragon();
d.setName("Smok Wawelski");				
                
entityManager.persist(d);                
entityManager.flush();
                
System.out.println("Zapisanie smoka o ID: " + d.getId()  );
                
h.setDragon(d);          
d.setRider(h);                
entityManager.persist( h );

Co skutkuje takim wyjściem w logach:

Hibernate: select max(id) from Dragon
Hibernate: insert into Dragon (name, id) values (?, ?)
Zapisanie smoka o ID: 1
Hibernate: select max(id) from HEROES
Hibernate: insert into HEROES (creationDate, dragon_id, co, gl, si, level, name, id) values (?, ?, ?, ?, ?, ?, ?, ?)

Powyżej wspomniałem, że mamy pewność, iż JPA spróbuje zapisać zmiany w bazie danych przy commicie transakcji. Jednakże nikt nie twierdzi, że nie może nastąpić to kiedy indziej. Jeśli provider stwierdzi konieczność wykonania operacji flush to po prostu to uczyni. Oczywiście ze względów na koszt dostępu do bazy danych nie następuje to przy każdej możliwej okazji, ale dobrze jest wiedzieć, że jest to możliwe.

Za jakiś czas wrócę do momentu zapisu zmian w bazie danych podczas operacji flush. Tymczasem zajmę się opisaniem dość istotnego z punktu widzenia JPA zagadnienia jakim jest detachment. Co to takiego? Ano jest to stan, w którym może znaleźć się encja. W jaki sposób do tego dochodzi? Tak naprawdę jest na to wiele sposobów.

  • application-managed persistence context, który zarządzał encją został zamknięty
  • w przypadku transaction-scoped entity managerów commit transakcji powoduje, że wszystkie encje w ramach persistence context przechodzą do stanu detached (spróbujcie usunąć encję smoka uprzednio kończąc transakcję i wykorzystać do tego istniejący obiekt – nic się nie stanie)
  • jeśli statefull session bean operujący na rozszerzonej transakcji zostanie zniszczony to wszystkie zarządzane przez niego encje zmieniają stan na detached
  • jeśli podczas commitu transakcji wystąpi błąd, a następnie rollback wówczas wszystkie zmienione encje zostają odłączone
  • użycie metod clear lub detach Entity Managera
  • serializacja encji – zserializowana postać encji jest odłączana od persistence context

Jak zatem widać w dotychczasowym kodzie też mieliśmy do czynienia z odłączonymi encjami nawet o tym nie wiedząc 😉 Przypomnijcie sobie fakt usuwania smoka po jego uprzednim zapisaniu – nigdy nie używaliśmy istniejącego obiektu, a zawsze od nowa pobieraliśmy jego nową instancję za pomocą metody find.

Jednakże co się stanie z encją jeśli przy próbie zakończenia transakcji zostanie rzucony wyjątek? W tym momencie nasze encje są niejako odłączane od persistence context, co określa się ładnym słowem – detach. Czy taka encja może się nam do czegokolwiek jeszcze przydać? Ano okazuje się, że może.

Co bowiem oznacza dokładnie fakt, iż encja jest w stanie detached? Nie jest już zarządzana przez JPA, co oznacza, że nie może być argumentem dla metod, które do poprawnego działania wymagają zarządzanej encji. Z drugiej strony cały stan encji wciąż jest w niej obecny – czyli nie ma problemu by ‘wyciągnąć’ z niej interesujące nas dane. Załóżmy, że wracamy do naszego smoka – w poprzednim wpisie zajmowaliśmy się usuwaniem encji z bazy danych. Teraz zrobimy to ponownie:

h.setDragon(null);
entityManager.remove(d);

// commit transakcji i ew. wyczyszczenie kontekstu w przypadku application-managed Entity Managera

System.out.println("Dragon: " + d.getId() + " - " + d.getName() )

Logi są dość oczywiste:

Hibernate: update HEROES set creationDate=?, dragon_id=?, co=?, gl=?, si=?, level=?, name=? where id=?
Hibernate: delete from Dragon where id=?
Dragon: 1 – Smok Wawelski

Jak widać mimo usunięcia obiektu z bazy danych (instrukcja DELETE) obiekt w aplikacji dalej zawiera potrzebne nam dane. Oczywiście problem pojawi się w sytuacji, w której potrzebowalibyśmy informacji o danych, które podlegają lazy-loading. Jest to dość powszechna sytuacja w aplikacjach sieciowych – pobieramy dane z bazy za pomocą JPA, po czym wynik (dość często lista encji) jest przekazywana w formie modelu do warstwy widoku. Tam iterujemy po naszej liście encji jednocześnie próbując odwołać się do własności encji, która podlega lazy-loading (dla przykładu jest to relacja many-to-many). Co wtedy? Nie da się tego do końca przewidzieć – zależy to od dostawcy.

Zaradzić problemom można na 2 sposoby (przynajmniej na razie):

  • zmienić sposób pobierania relacji na EAGER. Co jednakże nie zawsze ma sens ze względów wydajnościowych.
  • po pobraniu encji należy w kodzie (serwletu, usługi) odwołać się do wszystkich relacji, które chcemy następnie wykorzystać w warstwie widoku. Wygląda to dziko w kodzie, ale przynajmniej działa.

Nie są to specjalnie wygodne i ‘czyste’ rozwiązania. Dlaczego zatem nie zabrać się za problem z troszkę innej strony? Może w ogóle uniknijmy odłączenia encji od persistence context? Dzięki temu niezależnie do jakich własności/relacji nie odwoływalibyśmy się w widoku nie będziemy mieli problemu z otrzymaniem danych. W tym celu transakcję obsługuje się na warstwie serwletu, który obsługuje całe żądanie. Jednakże rodzi to kwestie związane z faktem, iż w każdym serwlecie musimy posiadać logikę, która otwiera i zamyka transakcję. Do tego dochodzą też kwestie związane z tym, iż dość często warstwę kontrolera obsługujemy za pomocą frameworków webowych (np. Spring MVC), który posiada swoje bajerki transakcyjne (patrz: Transakcje Spring z Hibernate i dobre prkatyki springowego @Transactional ). Zdecydowanie lepiej jest ten problem obchodzić korzystając z predefiniowanych zapytań JPQL, ale o tym napiszę w kolejnych artykułach. Na sam koniec warto wspomnieć, że możemy sprawdzić czy encja jest zarządzana poprzez metodę contains Entity Managera, które parametrem jest encja, a wartością zwracaną prawda lub fałsz.

Ponowne zarządzanie encją

Odłączona od persistence contextu encja może zostać ponownie do niego dołączona i stać się ponownie częścią wielkiej zarządzanej rodziny 😉 By to osiągnąć musimy wykorzystać metodę Entity Managera o nazwie merge. Rozpatrzmy taki kod:



entityManager.getTransaction().begin();  
                
Hero h = new Hero();

// CIACH - tu caly poprzedni kod

entityManager.persist( h );                
entityManager.getTransaction().commit();
		
entityManager.getTransaction().begin();
entityManager.clear();
entityManager.getTransaction().commit(); 
		                             
System.out.println( "Czy encja zarzadzana: " + entityManager.contains(h) );

Log aplikacji pokaże, że nasza encja (dzięki wywołaniu metody clear) nie jest już zarządzana w ramach persistence context. Jakiekolwiek zmiany teraz nie zostaną na niej dokonane – nie zostaną odzwierciedlone w bazie automatycznie, ale spróbujmy zastosować wspomnianą metodę merge.


// CIACH - kod z poprzedniego przykładu

entityManager.getTransaction().begin();
h.setName("Wmerdzowany Chlebikowy Mag");
entityManager.merge(h);
entityManager.getTransaction().commit(); 
                
entityManager.getTransaction().begin();
// Dodatkowe wyczyszczenie kontekstu - dzieki temu mamy 100% pewnosci, ze zmieniona encja zostanie pobrana z bazy danych
entityManager.clear();
entityManager.getTransaction().commit(); 
                
entityManager.getTransaction().begin();
Hero hh = entityManager.find(Hero.class, 1L);
System.out.println( "Imie maga: " + hh.getName() );                
entityManager.getTransaction().commit(); 

U mnie log powiedział krótko:

Imie maga: Wmerdzowany Chlebikowy Mag

Proces zapisu danych w bazie danych

Wspomniałem wcześniej o próbie zapisywania przez JPA informacji w bazie danych w momencie zakończenia transakcji. Istnieje jednakże możliwość programistycznego wymuszenia zapisu danych w bazie danych – dokonujemy tego za pomocą metody flush. Spowoduje ona próbę zapisania zmian odzwierciedlanych przez persistence context w bazie. Brzmi całkiem prosto, ale niestety z technicznego punktu widzenia takie nie jest – zwłaszcza jeśli mamy na myśli jeszcze nie istniejące w bazie danych encje. Poniżej krótkie przedstawienie zachowania zapisywanej encji w zależności od stanu encji oraz ustawień kaskadowych. Oto przykładowy fragment mapowań z encji bohatera:

@OneToOne(cascade={CascadeType.PERSIST}) 
@JoinColumn(name="dragon_id") 
private Dragon dragon;        
     
@OneToMany
@JoinTable(name="HERO_WEAPON",
        joinColumns=@JoinColumn(name="HERO_ID"),
        inverseJoinColumns=@JoinColumn(name="WEAPON_ID"))
private List<Weapon> weapons;

Zaś oto kod uruchomieniowy aplikacji:


 // Tworzymy encje broni
 entityManager.getTransaction().begin();
 Weapon w1 = new Weapon();
 w1.setName("Super-Duper-Miecz");
 entityManager.persist(w1);
 entityManager.getTransaction().commit();
 
 // Czyścimy kontekst - dzięki temu mamy pewność, że encja broni jest w stanie detached               
 entityManager.getTransaction().begin();                
 entityManager.clear();
 entityManager.getTransaction().commit();

 // Tworzymy bohatera jak do tej pory
 Hero h = new Hero();
 h.setCreationDate(new Date());
 h.setLevel(1);
 h.setName("Chlebikowy Mag");   

 FinanceState finances = new FinanceState();
 finances.setCooper(10);
 finances.setSilver(20);
 finances.setGold(1);
 h.setFinance(finances);
                
 entityManager.persist( h );                
 entityManager.getTransaction().commit();

W jakiej sytuacji jesteśmy? Posiadamy zapisaną encję bohatera. Do tego istnieje encja broni, która jest w stanie detached. Załóżmy, że chcemy dodać teraz smoka.

 entityManager.getTransaction().begin();
 Dragon d = new Dragon();
 d.setName("Smok Wawelski");				
 d.setRider(h); 
 entityManager.persist(d);              
  // Nie konczymy transakcji!!

Nasz smok został stworzony i jest zarządzany przez persistence context. Jednakże bohater nie wie nic o swoim smoku, nie ma też broni. Spróbujmy połączyć to zatem wszystko razem.

// Kontynujemy rozpoczętą transakcję

h.setDragon(d);

List<Weapon> weapons = new ArrayList<Weapon>();
weapons.add(w1);
h.setWeapons(weapons);

entityManager.persist(h);
entityManager.getTransaction().commit(); 

Przeanalizujmy powyższy kod. Encja bohatera jest w pełni zarządzana. Wyczesany miecz jest w stanie detached. Smok jest zarządzany, ale jest nową encją, która nie została jeszcze zapisana w bazie danych. Co zrobi JPA w powyższej sytuacji?

Przede wszystkim sprawdzi relacje obiektu Hero. Wpierw zajmiemy się smokiem – zasadniczo smok jest zarządzany, ale jest zupełnie nowym obiektem. Tym samym nie posiada jeszcze klucza głównego – w normalnych okolicznościach próby zapisania takiej relacji skończyłoby się wyjątkiem TransientPropertyValueException (w przypadku użycia Hibernate). Jednakże my oznaczyliśmy relację ze smokiem odpowiednimi atrybutami – CascadeType.PERSIST. Dzięki temu nawet niezapisany w bazie danych obiekt zostanie dzięki kaskadowości poprawnie utworzony oraz powiązany z owning side.

Drugim istotnym elementem jest lista broni jakie posiada nasz heros. Specjalnie wyczyściliśmy kontekst, aby mieć pewność, że encja broni będzie od niego odłączona. W deklaracji relacji w klasie Hero nie posiadamy jednakże jakiejkolwiek informacji o operacjach kaskadowych. Jak to zatem możliwe, że odłączona od kontekstu encja może być poprawnie przypisana do naszego bohatera? Jest to wyjątek w działaniu JPA – w przypadku relacji one-to-one lub many-to-one (z punktu widzenia encji broni – czyli posiada zaszyty klucz obcy w sobie) – taka operacja jest jak najbardziej możliwa.

Warto poświęcić chwilę na przetrawienie powyższego by zrozumieć, że zarządzanie encjami oraz zapisywanie relacji nie jest tak proste jak mogłoby się wydawać.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s