06 lutego 2007

Java Persistence - Rozdział 3.2 Cykl rozwojowy instancji encji

Jestem przekonany! Głos społeczności jest dla mnie równie ważny jak mój własny ;-) Zatem przyzwyczajam się powoli do lifecycle == cykl życia/rozwojowy, chociaż stadia rozwojowe byłoby równie dobre...chyba.

Co nam przynosi rozdział 3.2 Cykl rozwojowy instancji encji (Entity Instance’s Life Cycle)? Duuużo dobrej wiedzy, której zawsze mi brakowało. Niby oczywiste, ale diabeł tkwi w szczegółach, a te szczegóły później pojawiają się, kiedy już właśnie wychodziliśmy do domu przez finalnym oddaniem projektu, albo w innych temu podobnych sytuacjach. Istotne jest, że czytając specyfikację JPA w zasadzie można poskromić i inne rozwiązania, niekoniecznie związane z JPA. Pierwszym rozwiązaniem, które przychodzi do głowy (a jest związane z JPA) to Hibernate, ale są i inne - np. Apache Cayenne. Tak, czy owak, cieszę się, że w końcu doczekałem się specyfikacji EJB3, która cieszy i jednocześnie uczy dobrych praktyk (np. konieczność użycia interfejsów do eksponowania metod biznesowych), a nie zniewala w kajdany hierarchii klas, itp. Jestem na stronie 53 specyfikacji (z 256 dostępnych), więc chwila uniesienia zawsze wskazana dla poprawienia samopoczucia. W takim tempie analizy specyfikacji nie doczekam się końca przez gwiazdką (a gdzie tu czas na inne przyjemności duchowe?!).

Rozdział 3.2 opisuje sposób zarządzania cyklem życia instancji encji przez EntityManager (głównodowodzący w armii o kryptonimie JPA).

Wyróżniamy 4 stadia rozwojowe encji (ach, to tutaj powinienem użyć tego wyrażenia!):
  • nowy (właśnie stworzony, ang. new) - brak trwałego bytu (identyfikatora) i powiązania z kontekstem utrwalania
  • zarządzany (ang. managed) - odwrotność encji w stanie 'nowa', tj. instancja posiada trwały identyfikator oraz jest związana z kontekstem utrwalania
  • odłączony (ang. detached) - instancja encji z trwałym identyfikatorem, ale niezwiązana z kontekstem utrwalania (literacko możnaby ten stan opisać jako 'uwolniony od karbów kontekstu utrwalania' ;-))
  • usunięty/zniszczony (ang. removed) - instancja z trwałym identyfikatorem, związana z kontekstem utrwalania, która oczekuje usunięcia z bazy danych (innymi słowy, jest to analogiczne do stadium 'zarządzany' za wyjątkiem oznaczenia instancji do trwałego skasowania)
Poszczególne podpunkty opisują dokładnie efekt operacji EntityManager na encje w poszczególnych stadiach. Użycie elementu cascade w adnotacjach służy do propagowania działania operacji EntityManager na encji i jej powiązane relacją encje. Mechanizm cascade najczęściej wykorzystywany jest w relacjach typu rodzic-dziecko (gdzie istnienie dziecka jest nieuzasadnione w przypadku usunięcia rodzica).

3.2.1 Utrwalanie instancji encji

Nowa encja (w sensie jej cyklu rozwojowego) staje się zarządzana (a tym samym i utrwalona) przez wykonanie metody persist na niej lub jako efekt utrwalania rodzica (efekt elementu cascade, dalej zwany efektem "kaskadowym").

Działanie operacji persist
  • Jeśli encja jest nowa, staje się zarządzaną. Encja będzie zapisana do bazy w przed lub w trakcie zatwierdzenia tranzakcji lub w wyniku operacji flush.
  • Jeśli encja już istnieje i stan odpowiada stanowi w bazie, metoda persist ignoruje polecenie zapisu encji. Jednakże, operacja jest wykonywana kaskadowo na encjach z nią związanych, jeśli relacja, od jej strony, oznaczona jest cascade=PERSIST lub cascade=ALL (lub odpowiednio zdefiniowana w deskryptorze).
  • Jeśli encja jest usunięta, stanie się zarządzana
  • Jeśli X jest odłączona, rzucony może być wyjątek EntityExistsException podczas wywołania persist, a EntityExistsException lub podtypy PersistenceException podczas operacji flush lub zatwierdzenia tranzakcji
  • Dla wszystkich encji Y będących w relacji z X, jeśli relacja do Y została udekorowana cascade=PERSIST lub cascade=ALL, operacja persist zostanie wykonana na Y (wydaje się jakby powtórzono punkt z encją w stanie zarządzania, jednakże tutaj jest ogólniej i dotyczy pozostałych stanów, np. usunięty)
3.2.2 Usunięcie

Encja zarządzana przechodzi w stan usunięty po wywołaniu operacji remove lub w efekcie wykonania "kaskadowego" remove na rodzicu.

Działanie operacji remove
  • Jeśli encja jest nowa, metoda remove ignoruje polecenie skasowania encji. Jednakże, operacja jest wykonywana kaskadowo na encjach z nią związanych, jeśli relacja, od jej strony, oznaczona jest z cascade=REMOVE lub cascade=ALL (lub odpowiednio zdefiniowana w deskryptorze).
  • Jeśli encja jest zarządzana, encja przechodzi w stan usunięty. Operacja jest wykonywana kaskadowo na encjach z nią związanych, jeśli relacja, od jej strony, oznaczona jest z cascade=REMOVE lub cascade=ALL.
  • Jeśli encja jest odłączona, pojawi się wyjątek IllegalArgumentException bądź zatwierdzenie tranzakcji zakończy się niepowodzeniem.
  • Jeśli encja jest usunięta, metoda remove ignoruje ją (wywołanie metody remove nie ma żadnego wpływu na stan encji)
  • Usunięta encja będzie usunięta z bazy danych podczas lub przed zatwierdzeniem tranzakcji lub jako wynik operacji flush.
Po skasowaniu encji, jej stan (poza ogólnym stanem, jeśliby traktować instancję encji jak zwykłą instancję) będzie odpowiadał stanowi w momencie wywołania operacji remove.

3.2.3 Synchronizacja z bazą danych

Stan trwałych encji jest synchronizowany z bazą danych podczas zatwierdzenia tranzakcji (nie można było napisać, że zapis stanu instancji następuje w trakcie zatwierdzenia tranzakcji? Czyżbym coś ignorował takim stwierdzeniem? Chyba tak, ponieważ zmiany mogą nadejść od strony bazy danych. Czytam dalej oczekując dalszych wyjaśnień). Synchronizacja obejmuje zapis do bazy danych zmian w trwałym stanie encji i ich relacji z innymi, jak opisano wcześniej (patrz: punkty o persist i remove).

Uaktualnienie stanu encji obejmuje ustawienie nowej wartości do trwałego pola lub właściwości (dla przypomnienia: właściwość to pole z metodami operującymi na nim - setter/getter) bądź zmiany wartości modyfikowalnego, trwałego pola lub właściwości.

Synchronizacja z bazą danych nie obejmuje odświeżenia encji zarządzanych chyba, że explicite wywołano operację refresh (i tutaj jest odpowiedź na moje pytanie wyżej, gdzie zastanawiałem się o możliwości napłynięcia modyfikacji z bazy danych - warto było czekać!).

Dwukierunkowe relacje będą utrwalane opierając się na konfiguracji po stronie początkowej (właściciela) relacji. W gestii programisty pozostaje utrzymywanie, trzymanych w pamięci, poprawnych wartości referencji encji po ich zmianie, po obu stronach relacji. W przypadku jednokierunkowej relacji jeden-do-jednego lub jeden-do-wielu, obowiązkiem programisty jest zapewnienie, że semantyka relacji jest zgodna (np. podczas braku ustawienia unikatowości w mapowaniu, a jej obecności w bazie danych).

Pojawia się uwaga, o zapewnieniu poprawnego uaktualniania strony inicjującej (początkowej) relacji podczas modyfikacji po stronie końcowej relacji, aby zmiany zostały utrwalone podczas synchronizacji (hmmm, a o czym jest tutaj mowa?!)

Dostawca JPA ma prawo utrwalać zmiany (wykonywać synchronizację) w dowolnych momentach w trakcie trwania tranzakcji. Operacja flush wymusza synchronizację, jednakże dotyczy to wyłącznie encji związanych z kontekstem utrwalania (a przez to nie dotyczy to nowych, które nie są z nim związane). Metoda setFlushMode w EntityManager oraz Query umożliwia kontrolę wykonania synchronizacji.

Wyróżniamy 2 ustawienia trybu flush (klasa javax.persistence.FlushModeType):
  • AUTO - (domyślna wartość) synchronizacja następuje podczas wykonywania zapytań, gwarantując ich poprawne działanie - operacje Query (więcej w nadchodzącej sekcji)
  • COMMIT - synchronizacja następuje wyłącznie podczas zatwierdzania tranzakcji (porównując z Javadoc klasy FlushModeType wydaje się być w rozbieżności względem specyfikacji, która stwierdza, że dostawca JPA ma prawo wykonać synchronizację również w innych momentach trwania tranzakcji. Piszę na grupę!).
Jeśli tranzakcja nie jest aktywna, dostawcy nie wolno synchronizować stanu z bazą.

Działanie operacji flush
  • Jeśli encja jest zarządzana, następuje synchronizacja jej stanu z bazą danych:
    • Wszyskie encje z nią związane przez relację, która oznaczona jest od jej strony jako cascade=PERSIST lub cascade=ALL są również synchronizowane
    • Jeśli jakakolwiek encja Y związana z synchronizowaną encją poprzez relację nie oznaczoną cascade=PERSIST lub cascade=ALL, wtedy:
      • Jeśli encja Y jest nowa lub usunięta, rzucony zostanie wyjątek IllegalStateException podczas flush (i automatycznie nastąpi wycofanie tranzakcji) lub zatwierdzenie tranzakcji nie powiedzie się
      • Jeśli encja Y jest odłączona, działanie flush jest zależne od strony początkowej relacji (która spowodowała zaangażowanie encji Y). Jeśli X jest właścicielem relacji, dowolne zmiany w relacji są synchronizowane z bazą, w przeciwnym przypadku, jeśli Y jest właścicielem relacji, zachowanie flush jest nieokreślone (!)
  • Jeśli encja jest usunięta, jest ona kasowana z bazy danych. Wartości elementu cascade są nieistotne.
3.2.4 Encje odłączone

Przejście encji do stanu odłączony może nastąpić w wyniku zajścia następujących sytuacji:
  • Zatwierdzenie tranzakcji w przypadku użycia tranzakcyjnego zarządcy encji zarządzanym przez kontener/serwer aplikacyjny (ang. transaction-scoped container-managed entity manager) - więcej w kolejnej sekcji
  • Wycofanie tranzakcji - więcej w kolejnej sekcji
  • Wyczyszczenie kontekstu utrwalania (prościej: w wyniku wywołania metody clear, która wszystkie związane encje przenosi w stan odłączenia)
  • Zamknięcie zarządcy encji (EntityManager)
  • Wykonanie serializacji encji (bezpośrednio) bądź (pośrednio) przekazując encję przez wartość, np. jako rezultat wywołania metody zdalnego interfejsu komponentu
Odłączone encje działają w tym stanie jakby były zwykłymi klasami (można przyjąć, że są pozbawione świadomości swojego trwałego bytu, a przez to nie będą synchronizowane z ich stanem w bazie danych).

Aplikacje mogą korzystać z dostępnego stanu odłączonych encji po zakończeniu działania kontekstu utrwalania (w zasadzie to nie ma już znaczenia, bo i tak nie są z nim związane). Dostępny stan to:
  • Dowolne pole i właściwość nie określona przez fetch=LAZY
  • Dowolne pole i właściwość, które były pobierane przez aplikację (które zdążyły się zainicjować)
Jeśli trwałe pole/właściwość jest asocjacją, dostępny stan związanej instancji może być poprawnie pobrany jeśli związana instancja jest dostępna (Co to ma być?! Mam po tym więcej pytań niż odpowiedzi!). Dostępnymi instancjami są:
  • Dowolna instancja encji pobrana korzystając z operacji find
  • Dowolna instancja encji pobrana poprzez zapytanie lub bezpośrednio pobrana w klauzuli FETCH JOIN
  • Dowolna instancja encji, której zmienne instancji przechowująca trwały stan, inny niż klucz główny, były odczytywane przez aplikację
  • Dowolna instanacja encji, do której można dotrzeć z innej dostępnej instancji encji poprzez relację oznaczoną fetch=EAGER
3.2.4.1 Uwspólnienie stanu odłączonej encji

Operacja merge pozwala na dystrybucję stanu z odłączonej encji do encji trwałych zarządzanych przez EntityManager (czyli nowe odpadają, nieprawdaż?)

Działanie operacji merge
  • Jeśli encja jest odłączona, stan jest kopiowany do encji poprzednio związanej z kontekstem utrwalania dla tego samego identyfikatora lub tworzona jest nowa encja zarządzana
  • Jeśli instancja encji jest nowa, tworzona jest kopia zarządzana encji (wraz ze skopiowanym stanem)
  • Jeśli encja jest skasowana, rzucony zostanie wyjątek IllegalArgumentException podczas merge lub zatwierdzenie tranzakcji nie powiedzie się.
  • Jeśli encja jest zarządzana, operacja merge jest ignorowana. Jednakże, operacja jest wykonywana kaskadowo na encjach z nią związanych, jeśli relacja, od jej strony, oznaczona jest z cascade=MERGE lub cascade=ALL (lub odpowiednio zdefiniowana w deskryptorze)
  • Dla wszystkich encji Y będących w relacji z X, jeśli relacja do Y została udekorowana cascade=MERGE lub cascade=ALL, operacja merge zostanie wykonana kaskadowo na Y tworząca dla niej kopię Y', która z kolei będzie związana z kopią X' (chyba, że encja X jest już zarządzana, co nie spowoduje stworzenia dla niej kopii)
  • Jeśli encja X' jest zarządzaną encją powstałą w wyniku merge na X, będącej w relacji nieoznaczonej jako cascade=MERGE lub cascade=ALL do innej encji Y, wtedy asocjacja X' prowadzi do kopii Y' (to było trudne, zobaczymy jak będzie w praktyce)
Dostawcy JPA nie wolno uwspólnić pól/właściwości oznaczonych przez LAZY, których wartości nie zostały pobrane. Zostaną one zignorowane.

Kolumny oznaczone przez Version, z których korzysta encja muszą być zweryfikowane przez dostawcę podczas uwspólniania i/lub podczas flush bądź zatwierdzania tranzakcji. Brak kolumn Version nie implikuje weryfikacji (pierwszy raz, kiedy pojawiła się wzmianka o tych kolumnach, więc oczekuję wyjaśnień w nadchodzących rozdziałach).

3.2.4.2 Odłączone encje i opóźnione pobieranie/ładowanie (ang. lazy loading)

Nie zapewnia się przenośności encji w przypadku ich serializacji i ponownego przyłączania (uwspólniania stanu) z kontekstem utrwalania, gdy pola/właściwości i/lub relacje są oznaczone jako opóźnione (ang. lazy).

Wymaga się, aby dostawca JPA wspierał możliwość serializacji i kolejnych deserializacji i uwspólniania stanu odłączonych encji (z polami/właściwościami opóźnionymi, których wartości nie zostały pobrane) w osobnych wirtualnych maszynach, gdzie pracuje to środowisko dostawcy (wydaje mi się, że należy to odczytywać jako stwierdzenie, że zapisanie stanu instancji na dysk - nie do bazy danych - a następnie ponowne skonstruowanie encji odłączonej w innej wirtualnej maszynie Javy u innego dostawcy JPA może zakończyć się niepowodzeniem).

Rozwiązaniem problemu przenośności jest zaniechanie korzystania z opóźnionego ładowania.

3.2.5 Zarządzane encje

Obowiązkiem aplikacji jest zapewnienie, że instancja encji jest zarządzana wyłącznie przez pojedyńczy kontekst utrwalania. Nie gwaratuje się poprawności działania encji zarządzanej przez kilka kontekstów utrwalania.

Metoda contains określa, czy instancja encji jest zarządzana przez dany kontekst utrwalania i zwróci wartość true, jeśli:
  • Instancja encji została pobrana z bazy danych i nie została skasowana lub odłączona
  • Instancja encji jest nowa, jednakże metoda persist została dla niej wywołana bezpośrednio bądź pośrednio poprzez "kaskadową" relację (to ma znaczenie w przypadku tranzakcji jeszcze nie zatwierdzonej, gdzie stan encji nie musi być jeszcze w bazie danych - nie musi być zsynchronizowany)
Metoda contains zwróci wartości false, jeśli:
  • Instancja encji jest odłączona
  • Wywołana została operacja remove bezpośrednio bądź pośrednio poprzez kaskadową relację
  • Jeśli instancja jest nowa, ale metoda persist nie została dla niej wywołana bezpośrednio bądź pośrednio poprzez kaskadową relację (podobnie jak przy wartości true w punkcie 2, z tym, że tutaj instancji nie ma ani w bazie ani w kontekście utrwalania)
I na zakończenie pojawia się uwaga o "widoczności" instancji encji dla metody contains w przypadku operacji persist oraz remove (kaskadowych bądź bezpośrednich), których faktyczny zapis do bazy danych może odbyć się dopiero na zakończenie tranzakcji.

Uff, dużo przydatnej wiedzy. Aż się prosi, aby przygotować kilka przykładowych aplikacji, które mogłyby pomóc w zrozumieniu tematu. No cóż, jak człowiek zarobiony i ma wyłącznie 2 ręce (i w dodatku, jak mawia moja żona, lewe ;-)) to tworzenie przykładów odkładamy na...zakończenie tranzakcji, tj. lektury specyfikacji.

Do zobaczenia na spotkaniu Warszawa JUG!