08 lutego 2007

Java Persistence - 3.5 Encyjni obserwatorzy i metody przechwytujące

To nie było proste - znaleźć polski odpowiednik dla angielskiego listener. Słuchacz? Nasłuchiwacz? Nie bardzo. Ale obserwator? Dlaczego nie! Szczególnie, że mimo, że słuchacz (bliższy listener'owi) jedynie nasłuchuje, a obserwator może kojarzyć się jedynie z obserwowaniem lub oglądaniem, to i tak niesie dokładnie tą samą informację - reagowania na zdarzenia, a właśnie o to mi chodzi w tłumaczeniu. A może powinienem użyć słowa monitor encyjny? Dzisiaj postanowiłem zrelacjonować temat z użyciem słowa obserwator.

Dowolna metoda może przechwytywać zdarzenia związane ze stadiami rozwojowymi encji - metoda przechwytująca zdarzenia rozwojowe, w skrócie metoda przechwytująca (ang. lifecycle callback method).

Metoda przechwytująca może być zdefiniowana w klasie encji, mapowanej klasie bazowej (dla przypomnienia z poprzedniego wpisu z relacją lektury sekcji 2.1.9 o dziedziczeniu: mapowana klasa bazowa - klasa bazowa, która nie jest encją, choć dostarcza trwałego stanu i informacji o mapowaniu - jest bazą dla rozwoju innych bardziej zaawansowanych encji nie dostarczając kompletnej realizacji modelowanego bytu), lub klasa obserwatora (ang. entity listener class) związana z encją albo mapowaną klasą bazową. Klasa obserwatora encyjnego jest klasą, której metody są wywołane w odpowiedzi na zdarzenia rozwojowe encji (ang. entity lifecycle events). Dowolna liczba klas obserwatorów może być przypisana do encji bądź mapowanej klasy bazowej.

Domyślni obserwatorzy encji - obserwatorzy przypisani do wszystkich encji w jednostce utrwalania (ang. persistence unit) - są definiowani w deskryptorze XML.

Metody przechwytujące zdarzenia i klasy obserwatorów encyjnych są zdefiniowane za pomocą adnotacji lub deskryptora XML. Do przypisania klas obserwatorów encyjnych do klasy encji bądź mapowanej klasy bazowej służy adnotacja @EntityListeners. Kolejność wyzwalania obserwatorów wyznaczana jest przez kolejność ich deklaracji w adnotacji @EntityListeners. Deskryptor XML może służyć jako alternatywne miejsce do określania kolejności wyzwalania obserwatorów za pomocą adnotacji lub do zmiany kolejności wyznaczonej za pomocą adnotacji @EntityListeners.

Dowolny podzbiór lub kombinacja adnotacji może być przypisana do klasy encji, mapowanej klasy bazowej, czy klasy obserwatora. Pojedyńcza klasa nie może mieć więcej niż jednej metody przechwytującej dla stadium rozwojowego. Ta sama metoda może być zdefiniowana do przechwytywania wielu zdarzeń rozwojowych.

Dowolna ilość klas encji i mapowanych klas bazowych w hierarchi dziedziczenia może definiować klasy obserwatorów i/lub metody przechwytujące zdarzenia rozwojowe bezpośrednio (na klasie).

Klasa obserwatora encyjnego musi posiadać publiczny i bezparametrowy konstruktor.

Obserwator jest bezstanowy. Nie istnieje pojęcie cykli życiowych obserwatora.

Reguły funkcjonowania obserwatorów:
  • Metody obserwatora mogą rzucać niekontrolowane wyjątki (ang. unchecked/runtime exceptions), których wystąpienie w trakcie aktywnej transakcji, powoduje jej wycofanie.
  • Metody obserwatora mogą korzystać z usług środowiska - JNDI, JDBC, JMS oraz komponentów przemysłowych (ang. enterprise beans)
  • Metody obserwatora w przenośnej aplikacji, w ogólności, nie powinny wywoływać operacji EntityManager oraz Query, korzystać z innych instancji encji (czy to oznacza, że encje nie są traktowane jako komponenty przemysłowe w JPA?!) lub modyfikować relacje
Obserwatorzy encyjni wywołani w środowisku serwera aplikacyjnego współdzielą kontekst przestrzeni nazw JNDI związanej encji, a metody obserwatora są wywołane w kontekście transakcyjnym i bezpieczeństwa wywołującego komponentu w czasie, kiedy metoda została wywołana, np. jeśli zatwierdzenie transakcji nastąpi jako poprawnego wykonania metody biznesowej komponentu sesyjnego oznaczonej atrybutem transakcyjnym RequiresNew, metody obserwatora PostPersist oraz PostRemove są wykonywane w kontekście nazw, transakcyjnym i bezpieczeństwa tego komponentu sesyjnego.

3.5.1 Metody przechwytujące

Metody przechwytujące mogą być zdefiniowane na klasie obserwatora encyjnego i/lub bezpośrednio na klasie encji bądź mapowanej klasy bazowej.

Metody przechwytujące są oznaczone adnotacjami wskazującymi przechwytywane zdarzenia lub są zdefiniowane w deskryptorze XML.

Adnotacje używane do oznaczania metod przechwytujących na klasie encji bądź mapowanej klasie bazowej i na metodach obserwatora są takie same. Sygnatury pojedyńczych metod różnią się jednak (dla przypomnienia: podobnie było z metodami przechwytującymi, wtedy zwanymi metodami zwrotnymi, dla komponentów przemysłowych). Metody przechwytujące zdefiniowane w klasie encji bądź mapowanej klasy bazowej mają następującą sygnaturę:

void <DOWOLNA_NAZWA_METODY>()

, a zdefiniowane poza hierarchią dziedziczenia encji, tj. w dedykowanej klasie obserwatora:

void <DOWOLNA_NAZWA_METODY>(Object)

Parametr wejściowy typu Object jest instancją encji, dla której metoda będzie wywołana. Typ może być zadeklarowany jako właściwy typowi encji.

Metody przechwytujące mogą być public, private, protected lub domyślne, ale nie mogą być static czy final (chyba po to, aby umożliwić ich przysłanianie przez podklasy generowane przez dostawcę JPA).

Następujące adnotacje są dostępne do określenia metod przechwytujących odpowiednich typów:
  • @PrePersist
  • @PostPersist
  • @PreRemove
  • @PostRemove
  • @PreUpdate
  • @PostUpdate
  • @PostLoad
3.5.2 Działanie encyjnych metod przechwytujących

Metody udekorowane @PrePersist i @PreRemove są wywoływane zanim nastąpi wywołanie metod persist i remove dla danej encji, odpowiednio. Instancja encji, dla których wywołano merge, która z kolei spowoduje utworzenie nowej instancji zarządzanej encji wyzwoli metodę udekorowaną @PrePersist dla tej encji, po tym jak stan encji przyłączanej zostanie przekopiowany. Metody @PrePersist i @PreRemove będą kaskadowo wyzwolone na wszystkich powiązanych relacyjnie encjach. Metody @PrePersist oraz @PreRemove zostaną uruchomione podczas wykonania persist, merge i remove.

Metody @PostPersist oraz @PostRemove są wywołane dla encji właśnie utrwalonej bądź usuniętej bezpośrednio w bazie danych (co może nastąpić w momencie wywołania metod persist czy remove, po wywołaniu flush lub ostatecznie podczas zatwierdzania transakcji). Metody przechwytujące będą wywołane kaskadowo dla wszystkich powiązanych relacyjnie encji. Wartości generowanych kluczy głównych są dostępne w metodzie @PostPersist.

Metody @PreUpdate oraz @PostUpdate są wywołane przed i po wykonaniu operacji update bezpośrednio w bazie danych (co może nastąpić w momencie modyfikacji stanu instancji encji, po wywołaniu flush lub ostatecznie podczas zatwierdzania transakcji).
Specyfikacja nie gwarantuje wykonania @PreUpdate i @PostUpdate, kiedy encja jest utrwalona, a następnie zmodyfikowana w jednej transakcji lub kiedy encja jest zmodyfikowana, a następnie usunięta w trakcie jednej transakcji.

Metoda @PostLoad jest wywołana po załadowaniu stanu encji z bazy danych do aktualnego kontekstu utrwalania lub po wykonaniu refresh. Metoda @PostLoad jest wywołana zanim wynik zapytania jest zwrócony lub wykorzystany lub zanim nastąpi nawigacja (przejście) po relacji.

Specyfikacja nie gwarantuje kolejności wykonania metod przechwytujących na instancji encji przed lub po kaskadowym wykonaniu metod na jej relacyjnie powiązanych encjach.

Sekcja 3.5.3 przedstawia ciekawy przykład zaawansowanej encji Account wraz z obserwatorem AlertMonitor. Pierwszy raz spotykam się z tak zaawansowaną klasą encji. Zazwyczaj były to klasy z atrybutami (POJO) i adnotacjami mapującymi. Tym razem specyfikacja prezentuje klasę z obserwatorem z metodą @PostPersist, która przesyła informację o właśnie założonym koncie do systemu monitorującego oraz metodami przechwytującymi @PrePersist (sprawdzenie kwoty wpłaty przed otwarciem konta) i @PostLoad (wyliczającą nietrwały atrybut wskazujący na typ konta) w samej klasie encji.

3.5.4 Wiele metod przechwytujących dla pojedyńczego zdarzenia rozwojowego

Jeśli zdefiniowano wiele metod przechwytujących dla pojedyńczego zdarzenia rozwojowego, kolejność wykonania metod jest następująca:
  • Domyślni obserwatorzy, jeśli istnieją, są wykonani najpierw, w porządku wyznaczonym przez deskryptor XML. Domyślni obserwatorzy związane są ze wszystkimi encjami w jednostce utrwalania chyba, że wyłączono je przez adnotację @ExcludeDefaultListeners lub element exclude-default-listeners w deskryptrze XML.
  • Metody przechwytujące obserwatora zdefinowane dla klasy encji bądź mapowanej klasy bazowej są wywołane w kolejności definicji ich klas w adnotacji @EntityListeners.
  • Jeśli kilka klas encji bądź mapowanych klas bazowych w hierarchii dziedziczenia encji definiuje obserwatorów, obserwatorzy zdefinowani w klasach bazowych są wywołani zanim wywołani zostaną obserwatorzy klas potomnych w kolejności dziedziczenia, aż do samej klasy encji. Adnotacja @ExcludeSuperclassListeners lub element exclude-superclass-listeners może być wykorzystana w klasie encji bądź mapowanej klasy bazowej do wyłączenia wykonania obserwatorów klas bazowych dla encji bądź mapowanej klasy bazowej. Wyłączenie obserwatorów dotyczy klasy, w której znajduje się adnotacja @ExcludeSuperclassListeners (lub odpowiedni wpis w deskryptorze XML) i jej klas potomnych (wyłączeni obserwatorzy mogą być ponownie włączeni/uaktywnieni za pomocą ich deklaracji przez adnotację @EntityListeners bądź entity-listeners). Adnotacja @ExcludeSuperclassListeners (lub exclude-superclass-listeners) nie wyłącza domyślnych obserwatorów (tj. tych zdefiniowanych w deskryptorze XML).
  • Jeśli metoda przechwytująca dla pewnego zdarzenia rozwojowego jest zdefiniowana na klasie encji i/lub klasach bazowych (czy to klas bazowych encyjnych - będącymi również encjami, czy mapowanych klas bazowych), metody przechwytujące na klasie encji i/lub mapowanych klasach bazowych są wywołane po wywołaniu metod przechwytujących dla wcześniejszych klas bazowych (pod względem hierarchii dziedziczenia), tj. metody przechwytujące dla klas wyżej w hierarchii (bliżej typowi Object a nie klasie encji) są wywoływane wcześniej. Klasa ma prawo przesłonić odziedziczoną metodę przechwytującą o tym samym rodzaju przechwytywanego zdarzenia, co sprawi, że przesłonięta metoda nie będzie wywołana. Jednakże, jeśli metoda przesłania odziedziczoną metodę przechwytującą, ale definiuje inne zdarzenie do przechwycenia lub jeśli nie jest metodą przechwytującą, metoda przesłonięta będzie wywołana (ponieważ jest metodą przechwytującą zdarzenia innego rodzaju bądź w drugim przypadku w ogóle jest metodą przesłoniętą podczas, gdy przesłaniająca nie).
  • Tylko poprawne zakończenie wykonania metody przechwytującej spowoduje wykonanie kolejnych metod przechwytujących.
  • Deskryptor XML może służyć do zmiany kolejności wykonywania metod przechwytujących określonym przez adnotacje.
Sekcja 3.5.5 przedstawia przykład, który demonstruje kolejność wykonania metod przechwytujących encji i jej mapowanej klasie bazowej. Próba opisu przykładu spowoduje wyłącznie jego zaciemnienie. Warto zajrzeć do specyfikacji dla zapoznania się z nim.

3.5.6 Wyjątki

Metody przechwytujące mogą zakończyć się niekontrolowanymi wyjątkami. Niekontrolowany wyjątek powoduje wycofanie aktywnej transakcji, w której wykonywana jest metoda przechwytująca. Kolejne metody przechwytujące nie zostaną wywołane po wystąpieniu niekontrolowanego wyjątku.

3.5.7 Deklaracja obserwatorów i metod przechwytujących w deskryptorze XML

Deskryptor XML może być alternatywnym miejscem definiowania klas obserwujących i metod przechwytujących oraz kolejności ich wykonania (nadpisując konfigurację poprzez adnotacje).

Deklaracja metod przechwytujących

Element entity-listener w deskryptorze XML służy do deklaracji metod przechwytujących. Rodzaj zdarzenia definiowany jest przez elementy pre-persist, post-persist, pre-remove, post-remove, pre-update, post-update i post-load.

Co najwyżej jedna metoda obserwatora może być metodą przechwytującą dany rodzaj zdarzenia, bez względu, czy deklaracja następuje poprzez deskryptor XML, adnotacje, czy ich kombinację.

Deklaracja przypisania metod przechwytujących do encji

Podelement entity-listeners elementu persistence-unit-defaults definiuje domyślnych obserwatorów dla danej jednostki utrwalania.

Podelement entity-listeners elementów entity lub mapped-superclass definiuje obserwatorów dla encji lub mapowanej klasie bazowej, odpowiednio (wraz z ich klasami pochodnymi).

Przypisanie obserwatorów do klas encji jest addytywne (sumuje się). Obserwator przypisany do klasy bazowej encji lub mapowanej klasy bazowej jest również przypisany do nich.

Element exclude-superclass-listeners określa, że metody przechwytujące dla klas bazowych nie będą uruchomione dla klasy encji lub mapowanej klasy bazowej i podklas.

Element exclude-default-listeners określa, że domyślni obserwatorzy nie będą uruchomieni dla klasy encji lub mapowanej klasy bazowej i podklas.

Dosłowne wypisanie wyłączonych obserwatorów domyślnych lub klas bazowych dla danej encji lub mapowanej klasy bazowej powoduje, że wymienieni obserwatorzy będą wyłączeni dla tej encji lub mapowanej klasy bazowej i podklas.

Ostateczna kolejność wykonania metod przechwytujących dla pojedyńczego zdarzenia rozwojowego wyznaczana jest zgodnie z w/w regułami (opisanymi wyżej jako 3.5.4 Wiele metod przechwytujących dla pojedyńczego zdarzenia rozwojowego).

To była na prawdę interesująca sekcja! Przykłady były niebanalne i spotkałem się z tymi konstrukcjami po raz pierwszy. Możliwość przechwytywania zdarzeń była możliwa również w komponentach przemysłowych (wtedy nazywałem je biznesowymi). W poprzednich wersjach specyfikacji Java EE (1.4 i wcześniejszych) mechanizm przechwytywania był jedynie dostępny po wdrożeniu rozwiązań AOP (programowanie aspektowe, ang. aspect-oriented programming). Teraz nie trzeba się nimi zajmować (przynajmniej na początku), a jedynie postępować zgodnie z wytycznymi opisanymi w specyfikacji. Znaczne uproszczenie i większa przenośność aplikacji.

Kolejny rozdział to publiczny interfejs klasy Query - 3.6 Query API.

5 komentarzy:

  1. Dzieki za świetną robotę - Twój blog.

    <<<
    Metody @PostPersist [...] są wywołane dla encji właśnie utrwalonej [...] Wartości generowanych kluczy głównych są dostępne w metodzie @PostPersist.
    <<<

    Chciałbym to podkreślić, gdyż w właśnie w weekend mi się przydało. Spodziewałem się, że działanie tych adnotacji będzie analogiczne do wyzwalaczy "after" insert, update, remove.
    Tymczasem jest o drobinę inaczej. Trigger "after" nie zezwala na modyfikowanie danych rekordu. Metoda @postPersist możemy modyfikować dane obiektu (encji) - i dane te zostaną zapisane do bazy danych.
    Zachowaniem sprawdzilem na Glassfish + toplink.

    W twoim tekscie jest napisane "właśnie utrwalanej". To cholernie ważne zdanie! Właśnie utrwalanej - to zupełnie coś innego niż wskazywała by nazwa @postPersist.

    Jacek, czy mógłbyś podać orginał specyfikacji (po angielsku) abyśmy mieli pewność, że "post" mamy rozumieć jako "właśnie" a nie jako "po".

    Gdzie można wykorzystać "postPersist". Ja wykorzystuje w implementacji struktury drzewiastej. Napiszę o tym wkrótce w swoim blogu, http://www.jakubiak.eu/

    Pozdrawiam
    i dzieki za swietny tekst

    OdpowiedzUsuń
  2. Wycinek ze specyfikacji JSR 220: Enterprise JavaBeans, Version 3.0 - Java Persistence API, strona 59:


    The PostPersist and PostRemove callback methods are invoked for an entity after the entity has been made persistent or removed. These callbacks will also be invoked on all entities to which these operations are cascaded. The PostPersist and PostRemove methods will be invoked after the database insert and delete operations respectively. These database operations may occur directly after the persist, merge, or remove operations have been invoked or they may occur directly after a flush operation has occurred (which may be at the end of the transaction). Generated primary key values are available in the PostPersist method.


    Zaczynam mieć wątpliwości, czy dobrze to zreferowałem. Konieczny będzie krótki program weryfikujący. Chętni? ;-)

    Jacek

    OdpowiedzUsuń
  3. Moj program jest mniej wiecej taki:

    @Entity
    class Antek(
    @Id Long id;
    String nazwa;
    [..]
    @postPersist
    void zrobCos(
    nazwa = "Hello: " + getId().toString();
    );

    W tabeli: "antek" mam:
    id | nazwa
    1 | Hello: 1
    2 | Hello: 2

    Testowałem na Glassfish + Toplink + PostgreSQL.
    Ja też mam wątpliwości, czy metoda postPersist upoważnia mnie do modyfikowania encji? Niestety, na razie nie wiem jak inaczej to uzyskac. Działa, ale czy aby na pewno poprawnie.

    OdpowiedzUsuń
  4. Znalazłem inny przykład kodu, w którym encja jest modyfikowana w metodzie @PostPersist:
    http://www.devx.com/Java/Article/33650/1763/page/2

    OdpowiedzUsuń
  5. Kolejna ważna sprawa, to odpowiedź na pytanie w jaki sposób budowane są obiekty dla klas z metodami przechwytującymi, czyli:
    @EntityListener(KlasaPrzechwytuja.class)

    Zauważyłem, że Hibernate podczas budowania EntityManagerFactory tworzy jedną instancję klasy KlasaPrzechwytująca dla każdej zadeklarowanej metody (np.: @PostPersist) dla każdej klasy encyjnej (np.: @Entity @EntityListener). Instancje obiektów nasłuchujących nie są tworzone dla encji tylko dla klas encyjnych!!! Mam nadzieję, ale nie potrafię tego sprawdzić, że wywołania metod nasłuchujących są synchronizowane.

    OdpowiedzUsuń