Entity Manager i okolice – część 1

Do tej pory omawiając JPA dość pobieżnie wspomniałem o Entity Managerze. Jest to jednakże jeden z podstawowych elementów całego systemu persystencji i dlatego w dzisiejszym wpisie przyjrzymy się mu z większą uwagą.

Najczęstszym problemem związanym z używaniem JPA jest zrozumienie co tak naprawdę dzieje się z encją. Tudzież podstawowe pytanie brzmi – czy zmiany, które zostały wprowadzone zostały już odzwierciedlone w bazie danych? Pamiętam swoje pierwsze przygody z Hibernate w pracy zawodowej, kiedy dostawałem dziwne komunikaty, że entity is detached from context i co z tym trzeba robić. By zrozumieć cykl życia encji w ramach naszego kodu wpierw trzeba bardziej niż do tej pory przedstawić koncepcję EntityManagera, których to specyfikacja JPA dostarcza 3 typy:

  • container-managed – dobroci tego podejścia są nam dane tylko w przypadku używania serwera aplikacyjnego. By w klasie użyć EntityManagera zarządzanego przez kontener wystarczy, że opatrzymy własność naszej klasy adnotacją @PersistenceContext i w ten sposób mamy gotowego do działania EntityManagera. Nie musimy nic więcej robić – odpowiednio skonfigurowany plik persistence.xml zadziała cuda. To jednak nie koniec – EntityManager zarządzany przez kontener jest dostarczany w dwóch różnych wersjach.
    • transaction-scoped – najczęściej używany. Zwrócony przez kontener obiekt EntityManagera przed wykonaniem jakichkolwiek działań sprawdza, czy znajduje się on w obrębie aktywnej transakcji JTA. Jeżeli tak to (przy pierwszej operacji na z użyciem tego obiektu) jest tworzony PersistenceContext i jest dalej używany do czasu kiedy zakończy się transakcja.
    • extended – jest to typ, który jest dostępny tylko w przypadku statefull beans. Dzięki temu stan przechowywany przez beana jest utrzymywany przez szereg requestów za pomocą jednej konkretnej instancji persistence context (nie jest on niszczony przy każdym requeście, dopiero podczas usuwania beana).

    Obydwa powyższe typy możemy wstrzyknąć do naszych klas z pomocą wspomnianej adnotacji @PersistenceContext. W jaki sposób jednakże je odróżnić? Otóż jednym z atrybutów tej adnotacji jest type, który przyjmuje wartości typu wyliczeniowego PersistenceContextType. Jeżeli atrybut ten nie jest podany wówczas domyślnie używana jest wartość transaction-scoped. Na koniec tego krótkiego omówienia zachęcam do przeczytania pytania na StackOverFlow.

  • application managed – to typ EntityManagera, z którego korzystam tworząc przykłady. Przykładowy kod został przedstawiony w jednym z pierwszych wpisów. W tym przypadku to my sami tworzymy potrzebne obiekty, startujemy i zamykamy transakcję oraz zamykamy EntityManagera. Jest to jedyny sposób używania JPA poza serwerem aplikacyjnym. Oczywiście da się cały bloatware code uproscić choćby za pomocą Springa.

Transakcje

Transakcje są jednym z najważniejszych aspektów JPA. Do podziału przedstawionego powyżej dochodzą też kwestie związane z transakcyjnością, a dokładniej, z typem transakcji. W przypadku używania EntityManagerów zarządzanych przez kontener, domyślnie mamy do czynienia z transakcjami JTA. W przypadku kiedy używamy application-managed możemy wybrać sobie używany typ transakcji, choć domyślnie (w przypadku np. Javy SE) nie mamy dostępu do JTA zatem provider musi obsługiwać tylko natywne transakcje JDBC.

W tym miejscu dobrze jest zapoznać się z kilkoma pojęciami:

  • transaction synchronization – proces dzięki któremu po zakończeniu transakcji zmiany odwzorowane w persistence context zostaną poprawnie zapisane w bazie.
  • transaction association – to proces, w którym persistence context jest łączony z aktywną transakcją.
  • transaction propagation – jest to przekazywanie persistence context między kolejnymi transakcjami (w przypadku wersji extended)

Jeżeli nie widzisz różnicy pomiędzy synchronizacją, a asocjacją to wystarczy napisać, że te dwa procesy różnią się liczebnością. Można bowiem z jedną transakcją zsychronizować dowolną ilość persistence context, ale tylko jeden z nich może być obecnie aktywny (association).

Powinienem teraz napisać porównanie kodu dla container managed oraz application managed, ale przeglądając sieć znalazłem fenomenalny materiał Piotra Nowickiego, w którym poczytacie dokładnie o tym, co miałem opisywać.

Wspomnę w tym miejscu o cofaniu (rollback) transakcji. Jest to bowiem dość istotne, dotyczy też wszystkich operacji, które możemy wykonywać z pomocą EntityManagera. Otóż w przypadku kiedy operacja w bazie danych nie powiedzie się dostaniemy wyjątek, ale co najważniejsze nasze encje zostaną ‘odłączone’ (detached) z obecnego kontekstu! Jest to o tyle istotne, gdyż wiele osób łapie wyjątek po czym próbuje zapisywać encje jeszcze raz dziwiąc się, że dostaje dziwne błędy. Z całą pewnością takie coś nie zadziała. Natomiast można spróbować stworzyć nowy kontekst, podpiąć do niego te encje (napiszę o tym niedługo) po czym znów próbować zapisu. W takiej jednak sytuacji warto brać pod uwagę dwie rzeczy:

  • klucz główny – encje po cofnięciu transakcji są w takim stanie jakie ‘zastała’ je transakcja w bazie danych. Czyli o ile zmiany zostały wycofane z bazy danych, o tyle wartości w naszych encjach zostają ‘po staremu’. Jest to istotne w sytuacji, w której do generowania klucza głównego używamy sekwencji. Wówczas w bazie danych może ona zostać cofnięta o określoną wartość, ale nasza encja dalej posiada tę wartość jako klucz główny. Może okazać się zatem, że będziemy po chwili próbować zapisywać w bazie danych obiekt z ID, który już został użyty. By uniknąć tego typu sytuacji należy wyczyścić klucz główny przed ponownym zapisem.
  • pole wersjonujące – może mieć niepoprawną wartość.

Do tej pory używaliśmy API EntityManagera dość pobieżnie opisując metody służące zmianie encji. W tym miejscu wypada napisać kilka słów więcej, zwłaszcza mając na uwadze persistence context i transakcyjność.

Tworzenie encji

Metoda ta przyjmuje jako argument nowy obiekt, który dopiero chcemy dodać do kontekstu. Dlaczego nowy? Ano bo w przypadku próby zapisu istniejącej encji zachowanie aplikacji zależy od providera – na 99% poleci wyjątek, ale kiedy – to już pytanie. Może już w momencie dodawania obiektu do kontekstu, może dopiero przy końcu transakcji. W przypadku transaction scoped entity managera metoda ta musi być wykonywana przy aktywnej transakcji. Pozostałe dwa typy EntityManagera zaakceptują wywołanie metody, dodadzą do kontekstu, ale z jakimikolwiek działaniami poczekają do czasu kiedy pojawi się aktywna transakcja.

Wyszukiwanie encji

Wyszukiwanie encji jest chyba najczęściej wykonywaną operacją w przypadku baz danych. EntityManager dostarcza metodę find, którą mieliśmy już okazję używać. Wywołanie metody (oczywiście poprawne i zakończone sukcesem) zwraca zarządzaną encję. Jedynym wyjątkiem jest znów przypadek transaction scoped entity managera, gdzie zwrócona encja jest w stanie detached. Co ciekawe istnieje bardzo fajna metoda, którą poznałem dopiero teraz – getReference. Kod z książki jest moim zdaniem bardzo wiele mówiący.

Department dept = em.getReference(Department.class, 30);
Employee emp = new Employee();
emp.setId(53);
emp.setName("Peter");
emp.setDepartment(dept);
dept.getEmployees().add(emp);
em.persist(emp); 

Jak widać nie jest potrzebne wykonywanie dwa razy metody find – wystarczy metoda getReference by po prostu otrzymać odpowiedni obiekt tylko z kluczem głównym, który jednakże przyda się świetnie do stworzenia odpowiedniej relacji. Należy jednakże pamiętać, iż dobrze by było, aby encja taka istniała. Omawiana metoda zwraca proxy tej encji, zatem istnieje szansa, że przy próbie zapisu do bazy danych po zakończeniu transakcji zostanie zwrócony wyjątek, kiedy proxowana encja nie zostanie znaleziona. Warto o tym pamiętać.

Usuwanie encji

Wydawałoby się, że nie ma nic prostszego niż usuwanie encji. Podajemy ją jako argument metody remove i pufff, encji nie ma (oczywiście po commicie transakcji). W przypadku transaction-scoped entity managera kiedy spróbujemy usunąć niezarządzaną encję dostaniemy TransactionRequiredException. Pozostałe typy entity managera mogą usuwać niezarządzane encje, ale zmiana zostanie odzwierciedlona w bazie danych dopiero po commicie transakcji.

Problemem jest również usuwanie encji wraz z zdefiniowanymi w niej relacjami. Jest to temat powiązany z kaskadowością operacji, o czym napiszę w dalszej części. Na razie spróbujmy zabić smoka naszego bohatera – ustaliliśmy, że kiedy smok ubit wówczas nasz heros może sobie co najwyżej konia znaleźć. Zatem spróbujmy.

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
entityManager.remove(dd);

Uruchamiamy kod i co? Ano to:

Referential integrity constraint violation: “FK7ECFBA2844FC6C27: PUBLIC.HEROES FOREIGN KEY(DRAGON_ID) REFERENCES PUBLIC.DRAGON(ID) (1)”; SQL statement:
delete from Dragon where id=? [23503-145]

Dlaczego tak? Ano w naszym kodzie definiującym zależność między smokiem i bohaterem wskazaliśmy bohatera jako stronę rodzicielską. Zatem w tabeli z bohaterem znajduje się kolumna, która trzyma informację o identyfikatorze smoka. Kiedy spróbujemy usunąć smoka następuje naruszenie więzów integralności i dostajemy powyższy wyjątek. By tego uniknąć musimy wpierw ‘wyzerować’ relację, a dopiero potem ubić gadzinę. Powyższy kod powinien zatem wyglądać następująco:

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
hh.setDragon(null);
entityManager.remove(dd);

I dopiero teraz poprawnie da się usunąć naszego smoka. Zarządzanie tego typu zależnościami (przy usuwaniu) należy do aplikacji i jest zadaniem programisty.

Operacje kaskadowe

Domyślnie JPA nie robi niczego ponad to, czego jasno sobie zażyczyliśmy. Jeśli zatem jak w powyższym przykładzie chcemy usunąć encję czy też zależności między nimi musimy zrobić to jasno deklarując swoje zamiary. Jednakże ORM powstał by ułatwiać pracę programisty, a nie dodawać jeszcze więcej roboty. Stąd też istnieje możliwość poinstruowania JPA by pewne rzeczy robił za nas automatycznie.

Wracając do przykładu ze smokiem i herosem – załóżmy przez chwilkę, że smok powstaje wraz z naszym bohaterem (co w sumie zresztą do tej pory uskutecznialiśmy). Jednakże do tej pory tworzyliśmy oddzielnie instancję smoka, a następnie dodawaliśmy ją do naszego bohatera. Obydwie encje były zapisywane oddzielnie. Teraz zaś by osiągnąć ten sam efekt skorzystamy z dobrodziejstwa typu wyliczeniowego CascadeType, który podany jako atrybut dla adnotacji relacyjnych pozwala sterować zachowaniem operacji kaskadowych. Domyślną wartością tego atrybutu ( cascade ) jest brak jakichkolwiek działań. Jeśli jednak chcemy zmienić to zachowanie mamy do wyboru kilka opcji:

  • ALL – wszystkie operacje danej relacji będą kaskadowane
  • DETACH – tylko odłączenie encji od kontekstu persystencji
  • MERGE – aktualizacja encji
  • PERSIST – zapisanie encji
  • REFRESH – odświeżenie stanu encji (pobranie stanu z bazy danych i zastąpienie obecnego)
  • REMOVE – usuwanie encji

Dodajmy zatem odpowiednie wartości do naszego bohatera. Kod relacji wygląda teraz tak:

// Zwróc uwagę na nietypową składnię wartości - atrybut cascade musi mieć dostarczoną tablicę!!!
@OneToOne(cascade={CascadeType.PERSIST})  
@JoinColumn(name="dragon_id")
private Dragon dragon;

W kodzie wykonywalnym możemy dzięki temu zastosować taką konstrukcję:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");

Dragon d = new Dragon();
d.setName("Smok Wawelski");
				
h.setDragon(d);            
entityManager.persist( h );

I tym samym stworzyliśmy smoka jednocześnie tworząc bohatera. Nie ma potrzeby dla dwukrotnego wywoływania persist na każdej encji z osobna. Należy jednak zauważyć, że odwrotna zależność (zapisujemy smoka, a nie bohatera) nie jest możliwa. Operacje kaskadowe są wybitnie jednostronne (lub bezstronne – unidirectional) – spodziewane zachowanie musimy wskazać w każdym miejscu relacji. W naszym przykładzie kod encji smoka nie został zmieniony, ergo, przy zapisie smoka na pewno nie nastąpi kaskada operacji zapisu i tym samym przy próbie zapisu odwrotnego dostaniemy wyjątek:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");

Dragon d = new Dragon();
d.setName("Smok Wawelski");
d.setRider(h);

entityManager.persist( d );

Wyjątek:

java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance – save the transient instance before flushing: com.wordpress.chlebik.jpa.domain.Dragon.rider -> com.wordpress.chlebik.jpa.domain.Hero

O ile tworzenie obiektów poprzez operacje kaskadowe wydaje się być dość wygodnym rozwiązaniem, o tyle usuwanie encji będących w relacji z innymi brzmi w ogóle bardzo miło. Niestety w przypadku usuwania rekordów nie jest tak różowo. Z całą pewnością należy uważać co usuwamy i dlaczego, a także należy zdać sobie sprawę z sytuacji, o której pisałem w poprzednim paragrafie – nie istnieje możliwość stworzenia kaskadowej relacji między encjami, które pozwoliłoby usunąć dziecko z relacji z rodzicem bez usuwania rodzica. Czyli usunięcie smoka nie spowoduje tego, że zniknie zapisany klucz obcy w tabeli bohatera. Musimy tę operację przeprowadzić ręcznie. Gdybyśmy bowiem zapisali taki kod:

// Kod klasy Hero
@OneToOne(cascade={CascadeType.REMOVE}) 
@JoinColumn(name="dragon_id")
private Dragon dragon;

// Kod klasy Dragon
@OneToOne(mappedBy="dragon",cascade={CascadeType.REMOVE})
private Hero rider;

Zaś następnie w kodzie uruchomieniowym zastosowali takie coś:

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
entityManager.remove(dd);

Kod nie zrobiłby tego, czego od niego oczekiwaliśmy. W logu zapytań SQL widać to bardzo wyraźnie:

Hibernate: delete from HEROES where id=?
Hibernate: delete from Dragon where id=?

Czyli usuwając smoka usunęliśmy też jeźdźca. No nie do końca o to chodziło. Kiedy zaś usuniemy atrybut cascade z klasy Dragon, wówczas przy wykonaniu powyższego kodu (przynajmniej w najnowszej wersji Hibernate) nie wykona się nic! Dziwnym trafem wpisanie atrybutu cascade po stronie bohatera sprawia, że próba usunięcia smoka nie powoduje wygenerowania choćby próby skasowania rekordu z bazy danych (co w sumie może być dobre, albo i nie). Jeśli jednakże spróbujemy usunąć naszego bohatera w całości.

Hero hh = entityManager.find( Hero.class, 1l );
entityManager.remove(hh);

W logu SQL zobaczymy ciekawe rzeczy:

Hibernate: delete from Hero_nicknames where Hero_id=?
Hibernate: delete from HEROES where id=?
Hibernate: delete from Dragon where id=?

Jak widać JPA dokładnie przyjrzało się strukturze encji bohatera i usunęło wszystkie zależności (przydomki naszego bohatera są integralną częścią encji, niezależnie od faktu, że są kolekcją) w ten sposób, aby nie naruszać więzów integralności i tym samym zapewnić poprawność działania aplikacji. Jak widać usuwanie encji za pomocą operacji kaskadowych nie jest aż takie oczywiste 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