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.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s