Monthly Archives: May 2014

Cykl życia encji i wywołania zwrotne

Każdy programista mający styczność z Javą natknął się z całą pewnością z koncepcją ‘cyklu życia’ konkretnego obiektu/komponentu czy ziarna. Nie inaczej jest z encjami JPA. W zależności od dokonywanych na encji operacji mogą zostać wywołane fragmenty kody przypisane danym zdarzeniom (ang. lifecycle callbacks). Dziś o nich napiszę kilka słów.

Cykl życia encji

Cykl życia encji można dość łatwo zapamiętać korzystając ze znanego akronimu – CRUD (Create, Read, Update, Delete). Każda z tych operacji jest tłumaczona na konkretne instrukcje SQL, do nichzaś przypisane są zdarzenia mogące zajść przed oraz po operacji. Jedynym wyjątkiem są tutaj operacje odczytu. Czyli mamy takie zdarzenia:

  • PrePersist i PostPersist – wykonywane w momencie kiedy metoda persist() została wywołana na encji. Zdarzenie PrePersist może też być wywołane kiedy wywołano metodę merge() (na nowej encji), a także w przypadku nowych encji będących w relacji do obiektu, który ma operację kaskadową ustawioną na PERSIST. Z kolei zdarzenie PostPersist zachodzi nawet wtedy kiedy transakcja nie powiodła się (została cofnięta).
  • PreRemove i PostRemove – w momencie wywołania metody remove() odpalamy zdarzenie PreRemove (również w przypadku obiektów będących w relacji oznaczonych relacją kaskadową REMOVE). Kiedy ostatecznie jest wykonywana instrukcja SQL – DELETE wywoływane jest zdarzenie PostRemove. Podobnie jak w poprzednim punkcie wywołanie tego zdarzenia nie oznacza, ze operacja się udała.
  • PreUpdate i PostUpdate – w związku z tym, że nie jesteśmy pewni kiedy zostanie wykonana instrukcja aktualizująca stan encji w bazie, mamy tyle pewności, że PreUpdate zostanie wykonane przed wysłaniem instrukcji SQL do bazy. Sprawa z PostUpdate jest taka sama jak w poprzednich punktach.
  • PostLoad – zdarzenie wywoływane w momencie gdy encja została pobrana z bazy danych i fizycznie utworzona przez JPA. Obejmuje to również wywołanie metody refresh().

Skoro wiemy jakie mamy dostępne zdarzenia wypadałoby dowiedzieć się w jaki sposób możemy je wykorzystać. Wymaga to dwóch rzeczy – stworzenia metody wywoływanej przy okazji zdarzenia (w encji) oraz oznaczenia jej za pomocą adnotacji. Sygnatura tej metody może mieć dowolną nazwę – nie może jednak przyjmować jakichkolwiek parametrów, a także musi zwracać typ void. Metody final oraz statyczne nie wchodzą w grę, wyrzucanie checked exceptions też nie wchodzi w grę.

Metody musimy oznaczyć za pomocą adnotacji, które wskazują w przypadku którego zdarzenia ma być ona wywołana. Dostępne adnotacje znajdują się w pakiecie javax.persistence i odpowiadają nazwom zdarzeń, np: PostRemove. Tylko jedna adnotacja danego zdarzenia może wystąpić w ramach encji! Książka o metodach podpinanych do zdarzeń informuje w ten sposób:

Certain types of operations may not be portably performed inside callback methods. For example, invoking methods on an entity manager or executing queries obtained from an entity manager are not supported, as well as accessing entities other than the one to which the lifecycle event applies. Looking up resources in JNDI or using JDBC and JMS resources are allowed, so looking up and invoking EJB
session beans would be allowed.

 

 

Entity Listeners

 

Do tej pory pisałem o metodach, które znajdują się w konkretnej encji. Co jeśli chcielibyśmy logikę wykonywaną przy okazji zdarzeń uwspólnić i tym samym przenieść ją do innej klasy? Da się to oczywiście zrobić – służą do tego entity listeners. Podobnie jak w przypadku encji możliwe jest użycie w nich tylko raz jednej adnotacji danego zdarzenia. Jednakże każda encja może mieć ‘podpięte’ kilkanaście entity listeners, co pozwala na implementację dość rozbudowanej logiki. Oczywiście w związku z tym, iż metoda zdarzenia znajduje się poza docelową encją, sygnatura metod jest inna – musimy przecież w jakiś sposób dostarczyć do metody encję, na której będziemy operować. Encja ta jest parametrem metody, jej typ zaś musi być typem Object, typem encji lub interfejsem, który encja implementuje. Entity listeners muszą być bezstanowe, gdyż pojedyncze ich instancje mogą być współdzielone przez szereg encji.

Dodanie entity listeners do encji jest proste – dodajemy adnotację @EntityListeners do encji, a jako jej wartość podajemy listę klas, które zawierają interesujące nas metody. W przypadku zajścia zdarzenia dostawca JPA wpierw iteruje po listenerach wymienionych w powyższej adnotacji (w kolejności ich zapisu), a potem dopiero na końcu używa metody zapisanej w encji (o ile istnieje).

 

Dziedziczenie entity listeners

 

Na blogu do tej pory nie poruszyłem tematów związanych z dziedziczeniem encji, a także z mapowaniem wielu encji na jedną tabelę. Jednakże wiedza w tym temacie nie jest aż tak istotna by przedstawić zasady dziedziczenia entity listenerów i metod zdarzeń w encji – wystarczy wiedza o dziedziczeniu w ogóle.

Generalnie zasada jest prosta – deklaracje z klas wyżej w hierarchii są wywoływane przed tymi niżej w hierarchii. Dotyczy to zarówno deklaracji z użyciem adnotacji @EntityListeners jak i metod implementowanych w encjach. Jeżeli jednakże klasa nadrzędna posiada zaimplementowaną metodę obsługującą zdarzenie, wówczas istnieje możliwość jej nadpisania (ang. overriding) w klasie potomnej. Można wyłączyć wykonywanie odziedziczonych listenerów dodając do encji adnotację @ExcludeSuperclassListeners

Advertisements

Kolejna edycja kursu MongoDB

Dziś krótko – trochę reklamy i trochę informacji. Za kilka dni (27 maja) startuje kolejna edycja kursu MongoDB przeznaczona dla developerów Javy. W związku z tym, iż zmieniam pracodawcę, będę miał okazję pracować z tą technologią na co dzień, stąd postanowienie realizacji tegoż kursu. Ciekawostki i rzeczy warte opisania na pewno wylądują na blogu. Jeśli któryś z czytelników miał okazję realizować tenże kurs to proszę o jakiś feedback na co głównie zwrócić uwagę.

Walidacja encji

Dzisiaj ciągniemy dalej temat trochę bardziej zaawansowanych zagadnień powiązanych z JPA – napiszę coś o walidacji encji przy użyciu JSR 303.

Walidacja encji i JSR-303

Do tej pory poruszając temat encji skupialiśmy się głównie na zagadnieniach związanych z relacjami oraz ewentualnie na konkretnym opisaniem konkretnych właściwości (np. wymuszenie mapowania na konkretny typ Javy). Jednakże dość istotnym elementem każdej aplikacji bazodanowej jest walidacja przychodzących danych. By usprawnić ten dość często występujący proces powstała specyfikacja JSR 303 (jej protoplastą jest projekt hibernate-validator, który stał się od wersji czwartej implementacją wzorcową tej specyfikacji), której celem jest dostarczanie jednolitego sposobu walidacji danych w klasach Javy (nie dotyczy to tylko JPA). Zainteresowanych odsyłam do strony specyfikacji.

Zacznijmy od prostego, działającego przykładu, a następnie zajmę się szczegółami. Oczywiście wpierw trzeba ściągnąć odpowiedniego JARa. Obecnie najnowszą wersją hibernate-validator jest już wersja 5.0.2 ale jej nie będziemy używać!. Ściągamy wersję 4.3.1 i dodajemy ją do bibliotek projektu. Jest to jednakże (w przypadku aplikacji konsolowej) konkretna implementacja walidatora, zaś my potrzebujemy jeszcze klas, które dostarczają podstawowych adnotacji. Dlatego też musimy zaciągnąć jeszcze validation-api w wersji 1.0 (jest to ważne, wersja 1.1 nie jest kompatybilna z hibernate-validator w wersji 4). Dzięki temu będziemy używać adnotacji oraz klas typowych dla JPA, a nie Hibernate (w końcu egzamin dotyczy czystego JPA).

Idźmy dalej – w przykładach będziemy używać adnotacji, dzięki temu ułatwimy sobie robotę. Jednakże istnieje jak najbardziej możliwość korzystania z XMLa – by to osiągnąć należy umieścić odpowiedni plik XML w folderze META-INF – musi on nazywać się validation.xml. Na razie jednakże skupię się na adnotacjach, gdyż jest to bardziej czytelne rozwiązanie. Bibliotekę już mamy – teraz wypadałoby dowiedzieć się w jaki w ogóle sposób walidujemy encję?

Jest to dość proste – potrzebujemy do tego obiektu o typie Validator. W środowisku EE wystarczy wstrzyknąć go z użyciem adnotacji @Resource. W podawanych przeze mnie przykładach używamy kodu uruchamianego z konsoli, zatem musimy wykorzystać klasę Validation. Zachęcam jak zawsze do zapoznania się z dokumentacją tej klasy. Kod wygląda tak:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Tylko tyle i już jesteśmy w domu. Teraz zajmiemy się naszą encją – dla ułatwienia i czytelności przykładu dodamy walidacje do encji reprezentującej broń. Kod wygląda tak:

@Entity
public class Dragon {

    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;
    
    @Size(min=3,max=32)
    private String name;

    
    @OneToOne(mappedBy="dragon")
    @NotNull
    private Hero rider; 
    
    
    public Dragon() { }

    // reszta pominieta dla czytelnosci

}

Jak widać zostały dodane adnotacje dotyczące długości imienia smoka, a także dodatkowo wymusiliśmy, aby smok miał zawsze jeźdźca (co akurat do tej pory przechodziło). Teraz wypada zebrać to wszystko do kupy.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

entityManager.getTransaction().begin();
                                              
               
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);
             
                
Dragon d = new Dragon();
d.setName("Smok Wawelski");				
d.setRider(h);                                  
h.setDragon(d);
                
                                
entityManager.persist(h);
entityManager.getTransaction().commit(); 

Uruchamiamy projekt i co? Ano wszystko o dziwo się zapisało. Nie uruchomiliśmy ręcznie walidacji, zaś przy uruchomieniu aplikacji cały schemat jest generowany od nowa. Zatem jeśli spojrzeć w logi zobaczymy takie coś:

Hibernate: create table Dragon (id bigint not null, name varchar(32), primary key (id))

Czyli nasza tabela wzięła pod uwagę maksymalny rozmiar imienia smoka. Jeśli zmienimy tę adnotację (np. ustawimy maksymalną długość na 50 znaków, wówczas przy ponownym zbudowaniu projektu zmieni się też wpis w bazie danych). Jeżeli jednakże nasz kod troszeczkę zmodyfikujemy:

Dragon d = new Dragon();
d.setName("Smok Wawelski Wielki Wspanialy i Najlepszy w Swoim Fachu Pozeracz Dziewic");				
d.setRider(h);  

Wówczas przy próbie zapisu otrzymamy taki błąd:

Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [com.wordpress.chlebik.jpa.domain.Dragon] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
	ConstraintViolationImpl{interpolatedMessage='size must be between 3 and 37', propertyPath=name, rootBeanClass=class com.wordpress.chlebik.jpa.domain.Dragon, messageTemplate='{javax.validation.constraints.Size.message}'}
]

Zasadniczo mówi nam to tyle, że nasze adnotacje zadziałały. Jednakże nie jest zbyt przyjemnym obsługiwanie tego typu wyjątków – dlatego też obecnie przed zapisaniem smoka wykorzystamy obiekt walidatora:

Dragon d = new Dragon();
d.setName("Smok Wawelski Wielki Wspanialy i Najlepszy w Swoim Fachu Pozeracz Dziewic");				
d.setRider(h);  
                
Set<ConstraintViolation<Dragon>> errors = validator.validate(d);
for( ConstraintViolation<Dragon> err : errors ) {
    System.out.println(err.getMessage());
}
                                
h.setDragon(d);
                
if( errors.isEmpty() ) {
    entityManager.persist(h);
}

W konsoli dostaniemy prosty komunikat:

size must be between 3 and 37

Nie jest to jednakże brzydki wyjątek, który trzeba obsłużyć. Na chwilę obecną to tyle – mamy działający przykład walidacji.

Walidacja grupowa

Standardowo wszystkie adnotacje walidacyjne danej klasy otrzymują ‘dostęp’ o nazwie default. Dzieje się to w pełni przejrzyście i użytkownik nie musi o tym wiedzieć. Co jednak kiedy w pewnych przypadkach pewne własności powinny być walidowane, a w innych przypadkach nie (albo też walidatory mają mieć inne atrybuty). Wróćmy do przykładu naszego smoka – załóżmy, że chcemy zasymulować sytuację, w której smok dopiero się rodzi (czyli wykonamy na bazie polecenie INSERT). Wówczas zależy nam na tym, aby smok posiadał jeźdźca. Jednakże bohaterowi może się zemrzeć i wtedy nasz smok zostaje sam. I wówczas chcielibyśmy ten fakt zapisać w bazie (a obecnie nie możemy, gdyż daliśmy relacji z właścicielem adnotację @NotNull). Wówczas możemy oczywiście ręcznie nie weryfikować encji, ale to rodzi tylko problemy. Co więcej (wrócę jeszcze do tego zagadnienia) w większości aplikacji walidacja odbywa się niejawnie – automatycznie i nie wymaga ‘ręcznego’ uruchamiania.

Rozwiązaniem tego typu problemów jest przypisywanie konkretnych adnotacji do ściśle określonych grup. Grupy te tworzymy po prostu tworząc nowy interfejs rozszerzający interfejs Default o nazwie odpowiadającej grupie. Czyli dla naszego smoka możemy utworzyć dwa interfejsy (oczywiście w oddzielnych plikach):

import javax.validation.groups.Default;

public interface Create extends Default { }
public interface Modify extends Default { }

Dzięki temu deklarację relacji z jeźdźcem możemy teraz zapisać następująco:

 @OneToOne(mappedBy="dragon")
 @NotNull(groups=Create.class)
 @Null(groups=Modify.class)
 private Hero rider; 

Zaś w kodzie aplikacji po zgodnie właściciela smok może żyć sobie po swojemu:

entityManager.getTransaction().begin();
                
d.setRider(null);
                
Set<ConstraintViolation<Dragon>> errors = validator.validate(d, Modify.class);
for( ConstraintViolation<Dragon> err : errors ) {
  System.out.println(err.getMessage());
}
                
entityManager.getTransaction().commit(); 

Taki kod z całą pewnością nie spowoduje wygenerowania błędu. Jeżeli jednak drugi argument metody validate zmienimy na wartość Create.class – dostaniemy komunikat o błędzie. Mechanizm ten jest jak widać bardzo przydatny, do tego należy przypomnieć, że wartość atrybutu groups może przyjmować (zgodnie z nazwą) listę grup, podobnie jak drugi parametr metody validate(). Tym samym encję możemy oprogramować bardzo konkretnie, uwzględniając różnorakie wymagania.

Podstawowe adnotacje oraz tworzenie własnych

W specyfikacji wymieniono tylko kilka podstawowych adnotacji, które służą do walidacji. Oto one:

  • @Null
  • @NotNull
  • @AssertTrue
  • @AssertFalse
  • @Min
  • @Max
  • @DecimalMin
  • @DecimalMax
  • @Size
  • @Digits
  • @Past
  • @Future
  • @Pattern

Nie jest to jakaś gigantyczna lista. Jest tak między innymi dlatego, że w JSR 303 dodano możliwość tworzenia własnych, specyficznych adnotacji walidacyjnych. Zasadniczo temat dobrze zaprezentował Koziołek na swoim blogu, zatem nie będę dublował materiału i zainteresowanych odsyłam tamże.

Walidacja w JPA

Ok. Po tym przydługim wstępie należałoby dokładniej przyjrzeć się samej walidacji w JPA. Co prawda pokazałem już przykłady działającego kodu, ale były to przykłady sunny day scenario. Sama integracja pomiędzy JPA i JSR 303 jest lekko problematyczna. Specyfikacja walidacji ma tak naprawdę działać niezależnie od tego co i gdzie waliduje. Jak się to ma np. do opóźnionego ładowania własności encji? Łatwo można wyobrazić sobie sytuację, w której by poprawnie przeprowadzić proces walidacji trzeba do pamięci zaciągnąć całą encję, wraz ze wszystkimi zależnościami! Niezbyt często jest to pożądane zachowanie. Dlatego przy używaniu JSR 303 w stosunku do encji JPA walidacji podlegają tylko zwykłe własności oraz typy wbudowane. Relacje z innymi obiektami nie będą walidowane, chyba że encje, do których się odnoszą również są zmieniane/zapisywane/usuwane w tej samej transakcji.