Monthly Archives: October 2013

Dajcie kawałek SQLa czyli Java Persistence Query Language – Podstawy

Pamiętam pierwszą styczność z JPA i szczere zdziwienie jak fajnie może teraz być. Bez ręcznie klepanych SELECT * FROM. Jednakże jak to w życiu bywa z czasem okazało się, że o ile na co dzień używanie encji jest bardzo praktyczne, o tyle przy konieczności napisania jakiegoś typowo bazodanowego kodu (np. zmian w bazie danych wykonywanych cyklicznie na całym zbiorze danych) encje nie do końca są odpowiednim narzędziem. Autorzy JPA przewidzieli tego typu problemy zawczasu i stąd zaimplementowali możliwość używania zapytań. Jakich zapytań? Ano zapraszam do dalszej lektury.

Podstawy

Zaczniemy od operacji najniższego poziomu – chcesz używać czystego SQLa? Nie ma z tym problemu, da się, ale nie dziś o tym będzie mowa (na pewno o tym zagadnieniu napiszę w przyszłości). Dziś skupimy się na wynalazku o nazwie Java Persistence Query Language. Jest to technologia, którego początki leżą w mrocznych czasach EJB2. Od tamtego czasu rozwiązanie to trochę udoskonalono (delikatnie rzecz mówiąc). Dziś jest to sposób na pobieranie danych z bazy danych za pomocą składni przypominającej SQLa, ale niezależnej od dostawcy bazy danych oraz operującej na notacji obiektowej.

Niejasne? Nie dziwię się, kod więcej potrafi powiedzieć niż niejeden akapit tekstu.

SELECT h FROM Hero h

Co właśnie uczyniliśmy? Pobraliśmy listę wszystkich dostępnych bohaterów z tabeli. Dzięki zastosowaniu aliasu ‘h’ mamy punkt zaczepienia, z którego możemy iść dalej. Potrzebujemy tylko imion? Ależ proszę bardzo.

SELECT h.name FROM Hero h

Wykonując takie zapytanie otrzymamy listę tylko i wyłącznie imion herosów. Potrzebujemy imienia smoka, którego posiada nasz bohater? Problemu nie ma, za pomocą powyższej notacji możemy dostać się do dowolnej właśności encji.

SELECT h.dragon.name FROM Hero h

Myślę, że dość jasno już widać do czego może posłużyć nam JPQL. Jeżeli musimy pobrać dane posługując się trochę bardziej wyszukanymi kryteriami niż klucz główny mamy gotowe do zastosowania narzędzie.

Filtrowanie wyników i złączenia

Powyżej używaliśmy prostych zapytań, które pobierały wszystkie rekordy danego typu. Oczywiście możemy filtrować pobierane wyniki.

SELECT h FROM Hero h WHERE h.name = 'Chlebikowy Mag'

Wspomniałem, że używanie JPQL ma tę przewagę nad ‘wyciąganiem’ encji w tym, iż potrafi zredukować dość istotnie ilość pobieranych danych. Jeżeli chcemy wygenerować prostą tabelkę z danymi dotyczącymi encji nie jest koniecznym pobieranie jej oraz wszystkich jej zależności. Możemy osiągnąć cel w prostszy sposób:

SELECT h.name, h.level FROM Hero h 

Dzięki temu zwrócimy dane tekstowe bez konieczności obciążania bazy oraz łącza przesyłaniem niepotrzebnych nam danych. To jedna z największych zalet JPQL. Oczywiście jeśli potrzebujemy danych z innych encji będących w relacji z naszą (czyli po prostu na poziomie SQLa wykonujemy złączenie) również nie będzie problemu. Załóżmy, że poszukujemy bohatera, jednakże nie znamy jego imienia. Znamy za to imię jego smoka. Do dzieła.

SELECT h FROM Hero h, Dragon d WHERE d.name = 'Smok Wawelski'

Hibernate bez naszego udziału wygeneruje odpowiednie zapytania do bazy danych dzięki czemu będziemy mogli otrzymać poprawny rekord. Możemy skorzystać również z bardziej bezpośredniej składni.

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = 'Smok Wawelski'

Nie ma również problemu by skorzystać z oferowanych w SQL funkcji agregujących. Dla przykładu wyciągniemy liczbę wszystkich rekordów w tabeli.

SELECT COUNT(h) FROM Hero h

Dostępne są też standardowe funkcje jak MAX, MIN czy AVG. Istnieje również możliwość filtrowania wyników grupowania za pomocą instrukcji HAVING.

Powyższe przykłady pokazują tylko bardzo podstawową funkcjonalność JPQL. Póki co pokazałem podstawy, dzięki którym będzie można zaprezentować w jaki sposób używać JPQL z poziomu kodu. Dla niecierpliwych zaś polecam dokumentację JPQL na stronie Oracle, gdyż jej przepisywanie w formie krótkich kawałków kodu niespecjalnie ma sens. Zresztą sam JPQL pod kątem używanych słów kluczowych niespecjalnie różni się od czystego SQL.

Jak to uruchomić?

Jeżeli do tej pory zadawaliście sobie pytanie – wszystko ładnie, ale jak to uruchomić i przeprocesować wynik – oto nadeszła pora na odpowiedź. Wrzucanie kolejnych metod do Entity Managera by reprezentować obiekt zapytania nie byłoby sensowne (ileż to musiałoby być metod) – dlatego też stworzono dwa interfejsy – Query oraz TypedQuery (wprowadzony w JPA2). Wyróżniamy dwa typy zapytań – dynamiczne i predefiniowane (statyczne)

  • dynamiczne – jest tworzone w czasie działania programu
  • statyczne – jest definiowane w metadanych encji (za pomocą adnotacji lub XML), przewagą tego typu zapytań jest ich prekompilacja, a tym samym przyspieszenie wykonywania

By fizycznie wykonać zapytanie musimy wrócić do Entity Managera – posiada on metodę createQuery, której parametrami jest łańcuch tekstowy z zapytaniem (jeśli chcemy stworzyć instancję typu Query) oraz obiekt Class zwracanego wyniku (jeśli jesteśmy zainteresowani stworzeniem obiektu typu TypedQuery). Oto przykład:

String jpql = "SELECT h.name FROM Hero h WHERE h.id = 1";
TypedQuery query = entityManager.createQuery(jpql, String.class);
System.out.println("Imie bohatera: " + query.getSingleResult());

Wynikiem działania powyższego kodu jest:

Hibernate: select hero0_.name as col_0_0_ from HEROES hero0_ where hero0_.id=1
Imie bohatera: Chlebikowy Mag

Użyta metoda getSingleResult jest jedną z wielu zdefiniowanych przez interfejs Query. Polecam zapoznanie się z opisem tego interfejsu, gdyż przeklejanie dokumentacji niespecjalnie ma sens. Dla pełności wywodu zajmiemy się teraz zapytaniami statycznymi. Zaleca się ich stosowanie w sytuacjach, w których z góry wiadomo jaka będzie treść zapytania. Jeśli w naszej aplikacji będziemy dość często potrzebować listy bohaterów wraz z imionami ich smoków, wówczas jest to świetna okazja by wprowadzić zapytanie statyczne. We wszystkich wpisach operuję na adnotacjach zatem i tym razem nie będzie inaczej.

@NamedQuery(name="getAllHeroesNamesWithDragonNames",
            query="SELECT h.name, d.name FROM Hero h, Dragon d")
@Entity
public class Hero 

Jak widać podstawą jest adnotacja @NamedQuery. Atrybut query jest dość oczywisty, podobnie name. Jednakże w związku z tym ostatnim dobrze jest wspomnieć, iż nazwa jest rejestrowana w ramach danego persistence-unit. Dlatego też dobrze jest zapewniać ich unikalność by uniknąć ewentualnych konfliktów (np. poprzez stosowanie prefixów). Nie ma problemu by zdefiniować dla encji kilka zapytań, ale wtedy musimy posiłkować się adnotacją @NamedQueries, która przyjmuje tablicę z wartościami.

@NamedQueries({
      @NamedQuery(name="getAllHeroesNamesWithDragonNames",
                  query="SELECT h.name, d.name FROM Hero h, Dragon d"),
      @NamedQuery(name="getAllHeroesNamesWithLevelAndDragonNames",
                  query="SELECT h.name, h.level, d.name FROM Hero h, Dragon d")
})
@Entity
public class Hero 

W jaki sposób używać tego typu zapytań? Dość prosto:

 TypedQuery<Object[]> query = (TypedQuery<Object[]>) entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames");
 List<Object[]> res = query.getResultList();                
               
 for( Object[] o : res ) {
      System.out.println("Imie bohatera: " + o[0]  );
      System.out.println("Level bohatera: " + o[1]  );
      System.out.println("Imie smoka: " + o[2]  );
 }

Sam kod wyciągający wygląda dość brzydko (zauważ, że pobieramy wartości proste, a nie obiekty encji), ale reguła działania named-queries powinna być dość czytelna. Po raz kolejny zachęcam do zapoznania się z dokumentacją interfejsów zapytań – jest to kopalnia informacji.

No dobrze, a mogę dorzucić parametr?

Pewnie, że możesz. Co więcej – mamy znany choćby ze Springa mechanizm named-parameters, a także standardowe positional-parameters – tak jak w czystym JDBC. Jeden z poprzednich przykładów możemy zatem zapisać tak:

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = ?1

lub tak:

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = :dragonName

W jaki sposób podstawić wartości do odpowiednich parametrów? Znów musimy odwiedzić interfejs Query, który zawiera szereg metod setParameter (UWAGA! W przypadku użycia obiektów typu Date, Time lub Timestamp wymagany jest 3 parametr typu wyliczeniowego TemporalType ).

String jpql = "SELECT h FROM Hero h, Dragon d WHERE d.name = :dragonName ";                
                
Hero hh = entityManager.createQuery(jpql, Hero.class)
                       .setParameter("dragonName","Smok Wawelski")
                       .getSingleResult();
                
System.out.println("Imie bohatera: " + hh.getName()  );

Viola! Działa. Zadziałałoby również w przypadku zamiany :dragonName na ?1 oraz wywołania metody setParameter(“dragonName”,”Smok Wawelski”) na setParameter(1,”Smok Wawelski”). Z góry jednakże widać, że używanie named-parameters jest zdecydowanie bardziej czytelne.

Wykonywanie zapytań – co jeszcze?

Wróćmy do pobierania danych przez zapytanie. Jak widać było na powyższych listingach rezultatem zwracanym przez zapytanie JPQL mogą być:

  • typy proste – Stringi, prymitywy czy typy JDBC
  • encje
  • tablica obiektów
  • typy zdefiniowane przez użytkownika (nie encje)!

Z typami prostymi i tablicą obiektów mieliśmy już styczność, z encjami zasadniczo też. W przypadku encji istotnym jednakże jest udzielenie odpowiedzi na pytanie – w jakim stanie otrzymujemy ową encję? W przypadku pobieraniu danych za pomocą metody find Entity Manager dostajemy zarządzaną encję. Dokładnie to samo zachowanie będziemy mogli zaobserwować w przypadku rezultatu zapytania JPQL. Zwrócona encja jest zarządzalna i ewentualne zmiany tej encji w ramach wciąż istniejącej transakcji zostaną odwzorowane w bazie danych. Jedynym wyjątkiem jest transaction-scoped Entity Manager – jeżeli jest użyty poza transakcją wszystkie pobrane encje od razu są pobierane w stanie detached. Co prawda możemy je potem podpiąć do kontekstu metodą merge, ale należy o tym pamiętać! Z drugiej strony, zwracanie encji w stanie detached ma niezaprzeczalną zaletę – o wiele mniej obciąża pamięć. Jeśli zatem jedynym celem działania naszej metody jest pobranie wyników do ich zaprezentowania (tylko i wyłącznie), dobrze byłoby z góry zaplanować metodę pobierającą beana jako nie korzystającą z transakcji (za pomocą adnotacji @TransactionAtribute z wartością NOT_SUPPORTED).

Ostatnim punktem na liście powyżej są typy zdefiniowane przez użytkownika, ale nie będące encjami. Wróćmy do naszego przykładu pobierającego imiona bohaterów, ich poziomy oraz imiona smoków.

 TypedQuery<Object[]> query = (TypedQuery<Object[]>) entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames");
 List<Object[]> res = query.getResultList();                
               
 for( Object[] o : res ) {
      System.out.println("Imie bohatera: " + o[0]  );
      System.out.println("Level bohatera: " + o[1]  );
      System.out.println("Imie smoka: " + o[2]  );
 }

Operowanie po tablicach obiektów nie wygląda zbyt profesjonalnie. Dlatego też jeśli chcemy uczynić powyższy kod bardziej czytelnym możemy zwrócony przez zapytanie wynik opakować w specjalnie do tego celu stworzoną klasę (przypomina to trochę użycie RowMappera JDBC). Stosuje się do tego słowo kluczowe NEW w treści zapytania SELECT. Wpierw utwórzmy prostą klasę opakowującą:

public class HeroNameLevelDragonNameWrapper {
    
    private String heroName;
    private Integer level;
    private String dragonName;
    
    public HeroNameLevelDragonNameWrapper(String heroName, Integer level, String dragonName ) {
        this.dragonName = dragonName;
        this.heroName = heroName;
        this.level = level;
    }

    // GETTERY I SETTERY POMINIETE
}

Klasa już jest, teraz należy zmienić treść zapytania (zwróć uwagę na słówko NEW oraz podanie pełnej nazwy klasy).

@NamedQueries({     
      @NamedQuery(name="getAllHeroesNamesWithLevelAndDragonNames",
                  query="SELECT NEW com.wordpress.chlebik.jpa.domain.HeroNameLevelDragonNameWrapper(h.name, h.level, d.name) FROM Hero h, Dragon d")
})

Zaś w kodzie uruchomieniowym możemy teraz użyć takiej konstrukcji:

TypedQuery<HeroNameLevelDragonNameWrapper> query = entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames",HeroNameLevelDragonNameWrapper.class);
List<HeroNameLevelDragonNameWrapper> res = query.getResultList();                

for( HeroNameLevelDragonNameWrapper o : res ) {
    System.out.println("Imie bohatera: " + o.getHeroName()  );
    System.out.println("Level bohatera: " + o.getLevel()  );
    System.out.println("Imie smoka: " + o.getDragonName()  );
}

Co jest o wiele bardziej czytelne i zgodne z dobrymi praktykami.

Jak wspomniałem na początku wpisu – używanie JPQL pozwala na poprawienie wydajności wykonywanych zapytań. Pomijając fakt, iż nie musimy budować całego drzewa zależności pobieranych encji, bardzo dobrą praktyką jest również paginacja zwracanych wyników. Chyba każdy w swojej karierze zawodowej prędzej czy później był zmuszony zaimplementować jakąś formę paginacji wyników. Podstawowym problemem jest zawsze ilość danych, które pobieramy. najmniej wydajnym rozwiązaniem jest pobranie wszystkich wyników, a następnie ‘obrabianie ich’ programistycznie. Jest to o tyle nieefektywne, iż w 99% przypadków użytkownik nie musi przeglądać niczego poza pierwszą stroną. Obciążenie pamięci i bazy jest tym samym zupełnie nieefektywne.

Do sensownego nawigowania po dużych zbiorach danych interfejs Query (i jego potomkowie rzecz jasna) udostępnia metody setFirstResult oraz setMaxResults. Ta pierwsza ustawia punkt początkowy, od którego będziemy pobierać dane zwrócone przez zapytanie, zaś druga metoda określa ilość rekordów, którymi jesteśmy zainteresowani. Sposób implementacji zachowań tych metod zależy od dostawcy JPA dlatego dobrze jest zapoznać się z dokumentacją używanej biblioteki, aby mieć pewność poprawnego działania.

Podpowiedzi

twórcy JPA stanęli w pewnym momencie w tym samym miejscu, w którym prędzej czy później lądują twórcy specyfikacji czy standardów. Skoro tworzony jest nowy standard, a do tej pory mieliśmy kilkunastu dostawców podobnych rozwiązań, w jaki sensowny sposób spróbować zunifikować różnice między nimi? Istnieje również drugi problem – standard standardem, ale zazwyczaj twórcy implementacji dodają z chęcią coś od siebie (w sumie to jedna z głównych przyczyn różnic między dostawcami JPA). By ułatwić rozszerzanie funkcjonalności JPA bez konieczności zmiany API dodano możliwość dodawania do zapytań ‘podpowiedzi’ (ang. hint).

Podpowiedzi można ustawić dla zapytania dwojako – albo programistycznie wywołując metodę setHint, albo też poprzez metadane (w adnotacji NamedQuery). Pełna lista podpowiedzi jest zależna od dostawcy. W przypadku Hibernate istnieje do tego nawet specjalna klasa QueryHints, która zawiera stosowne wartości. Zerknijmy na przykład:

TypedQuery<HeroNameLevelDragonNameWrapper> query = entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames",HeroNameLevelDragonNameWrapper.class);
query.setHint(QueryHints.TIMEOUT_JPA, 5000L);
List<HeroNameLevelDragonNameWrapper> res = query.getResultList();                

W ten sposób możemy podpowiedzieć JPA szereg różnych rzeczy – jak choćby powyższy timeout zapytania, sposób pobrania wierszy czy dodanie komentarza do zapytania.

Operacje masowe

Tak zwane bulk-operations dotyczą wykonywania instrukcji UPDATE oraz DELETE na wierszach bazy danych. Mogą być one bardzo użyteczne, ale przy ich użyciu dobrze jest zrozumieć jedną zasadę – You are alone 🙂
Operacje tego typu wykonujemy następująco (tak, wiem że kod jest prosty):

String jpql = "UPDATE Hero h SET h.name = :newName WHERE h.id = :id";                
                
entityManager.createQuery(jpql)
             .setParameter("newName","Nowy lepszy Chlebikowy Mag")
             .setParameter("id",1L)
             .executeUpdate();                

Co jest problematycznego w tym zapytaniu? Ano to, że ewentualnie wcześniej pobrane do persistence-context dane niekoniecznie muszą zostać odświeżone! Jest to bardzo istotne, co zresztą pokaże ten kod i jego rezultat.

Hero hh = entityManager.find(Hero.class, 1L);
System.out.println("Pobranie bohatera");
String jpql = "UPDATE Hero h SET h.name = :newName WHERE h.id = :id";                                

entityManager.createQuery(jpql)
             .setParameter("newName","Nowy lepszy Chlebikowy Mag")
             .setParameter("id",1L)
             .executeUpdate();

System.out.println("Zapisanie zmiany bohatera");                                
System.out.println("Imie bohatera: " + hh.getName()  );

Oto konsola:

Pobranie bohatera
Hibernate: update HEROES set name=? where id=?
Zapisanie zmiany bohatera
Imie bohatera: Chlebikowy Mag

Zmiana nie została odwzorowana w persistence-context. Dlatego w przypadku transakcji zarządzanych przez kontener najlepiej jest zapewnić wykonywanie tego typu operacji (to samo jest z operacjami DELETE) w oddzielnej transakcji – pobierać zaś wyniki, które mogłyby być zmienione przez taką operację dopiero po jej wykonaniu.

Na sam koniec zaś odniosę się też do operacji DELETE – tworzone i wykonywane poprzez JPQL nie podlegają kaskadowości! Tym samym owszem – być może usuniemy wskazane encje, ale relacje opatrzone adnotacjami CascadeType.PERSIST pozostaną w bazie danych. Dlatego raz jeszcze – You are alone.

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ć.

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ć.