Category Archives: Hibernate

Zaawansowane zagadnienia ORM – dziedziczenie encji

To już ostatni materiał z książki dotyczący JPA jako takiego. Temat jednak dość istotny, zwłaszcza w różnego typu legacy-systems, których model danych potrafi być delikatnie mówiąc – zagmatwany. Napiszę dziś o dziedziczeniu encji.

Podstawy

Encje jako takie są zwykłymi klasami Javy. Tym samym jak najbardziej mogą rozszerzać inne klasy, a także być rozszerzane. Model obiektowy pozwala bardzo efektywnie przedstawić złożoność rozwiązywanych problemów biznesowych poprzez sensowne zastosowanie dziedziczenia. Z drugiej zaś strony mamy relacyjną bazę danych, która opiera się o tabele i relacje. Nie za wiele wie ona o dziedziczeniu. Istnieje kilka sposobów na rozwiązanie tego problemu.

Podstawowym pojęciem przy dziedziczeniu encji jest mapped superclass. Jest to klasa oznaczona adnotacją @MappedSuperclass. Oznaczenie nią encji spowoduje, że jej własności i relacje zostaną odzwierciedlone w encjach dziedziczących po tej klasie (i nie chodzi o same pola Javy, ale o rozpoznanie tego faktu przez JPA). Jednakże klasa taka sama w sobie nie posiada oddzielnej tabeli w bazie danych! Klasa taka nie musi być również deklarowana jako abstrakcyjna, jednakże jest to dobra praktyka – klasa taka nie powinna mieć instancji – powinna być reprezentowana tylko i wyłącznie przez klasy dziedziczące po niej. Klasy oznaczone tą adnotacją nie mogą również być używane do wyszukiwania oraz w zapytaniach (np. jPQL). Służą tylko jako opakowanie podstawowych własności w jednym miejscu, tak by klasy po niej dziedziczące zawierały tylko specyficzne siebie pola czy zachowania.

Należy wspomnieć, że same encje też mogą być abstrakcyjne. Możemy ich normalnie używać w aplikacji, jedynie nie możemy fizycznie tworzyć ich instancji, ale mogą bez problemu służyć jako typ obiektu, który po nich dziedziczy.Zanim przejdę do przykładów kodu dodam jeszcze, że dziedziczenie przez encje po zwykłych klasach Javy jest oczywiście możliwe. Te zwykłe klasy nazywa się transient-classes – oczywiście dziedziczy się po nich zachowanie oraz stan, ale nie są one odzwierciedlane w bazie danych.

Poznaliśmy już adnotację @MappedSuperclass. Wspomniałem również, iż nie posiadają one swojej reprezentacji w bazie danych (w formie tabeli). Dlatego też za podstawę ‘bazodanową’ dziedziczenia uznaje się klasę będącą encją. Może ona rozszerzać klasy oznaczone @MappedSuperclass, ale oczywiście nie musi. W specyfikacji JPA zostały przedstawione trzy strategie realizacji dziedziczenia po stronie bazy danych. Są to Single-Table Strategy, Joined Strategy oraz przedstawiona, ale niewymagana do implementacji przez dostawców Table-per-Concrete-Class Strategy. Przedstawię je poniżej opierając się na przykładzie encji broni, którą może używać bohater w naszej wzorcowej implementacji gry RPG. Do tej pory broń była reprezentowana przez jedną encję – Weapon. Dziś dodamy trochę urozmaicenia.

Single-Table Strategy

Zgodnie z nazwą dziedziczenie jest realizowane w oparciu o jedną tabelę w bazie danych. Automatycznie implikuje to istnienie w niej kolumn, które będą w stanie przechować stan wszystkich encji, które realizują dziedziczenie w oparciu o tę metodę. Przerobimy zatem naszą encję reprezentującą broń na trzy encje, które będą reprezentowały różne typy broni.

@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="WEAPON_TYPE",discriminatorType=DiscriminatorType.STRING)
public abstract class Weapon  {

    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;
        
    private String name;   
    
    // Getterki i setterki ominiete	
}

@Entity
@DiscriminatorValue("StingWeaponDiscValue")
public class StingWeapon extends Weapon {

      private int stingAttack;

      // Getterki i setterki ominiete
}

@Entity
@DiscriminatorValue("RangeWeaponDiscValue")
public class RangeWeapon extends Weapon {

      private int rangeAttack;

      // Getterki i setterki ominiete
}

Jak widać trochę się tutaj dzieje. Zaczniemy od encji Weapon – jest to klasa abstrakcyjna, która jednakże zostanie odwzorowana na tabelę, gdyż posiada adnotację @Entity. Jest ona również oznaczona adnotacją @Inheritance, która pozwala określić typ użytej strategii dziedziczenia. Jeżeli adnotacja ta nie zostanie użyta, albo też nie zostanie podany typ strategii, wówczas zostanie zastosowany domyślny typ czyli właśnie Single-Table Strategy. By jednak pokazać możliwości tej adnotacji w powyższym przykładzie podałem wartość tego atrybutu. Użyta została też adnotacja @DiscriminatorColumn. Określa ona nazwę kolumny w tabeli, w której będzie przechowywana informacja o klasie, która ma zostać użyta do stworzenia instancji encji. Podajemy też typ kolumny – domyślnie jest to podana przeze mnie wartość STRING, ale możliwe są też INTEGER oraz CHAR.

Poszczególne klasy dziedziczące po encji broni są dość proste – dodajemy tylko jedną własność określającą wartość punktową zadawanego ataku. Jedyną nowością jest dodanie adnotacji @DiscriminatorValue, której znaczenie jest dość oczywiste. W przypadku jej braku dostawca JPA użyje nazwy klasy. To wszystko. Teraz w kodzie aplikacji możemy stworzyć takie cos:

entityManager.getTransaction().begin();
                
StingWeapon sword = new StingWeapon();
sword.setName("Super-Duper-Miecz");
sword.setStingAttack(10);
entityManager.persist(sword);
                
RangeWeapon bow = new RangeWeapon();
bow.setName("Super-Duper-Dlugi-Luk");
bow.setRangeAttack(10);
entityManager.persist(bow);                                            
                
entityManager.getTransaction().commit();

W bazie danych po uruchomieniu powyższego otrzymamy taki efekt:

ZrzutBroni

Jak widać niewykorzystane kolumny zostały wypełnione wartościami NULL. Kolumna zawierająca informacje o typie klasy została wypełniona automatycznie wartościami, które określiliśmy w definicji encji. Wszystko pięknie działa 😉

Joined Strategy

Zastosowanie strategii jednej tabeli może być na dłuższą metę dość zasobożerne. Nagle okazuje się, że w przypadku rozbudowanych hierarchii nasza tabela zawiera dużą ilość kolumn, które w znacznej mierze zawierają wartości NULL. Jeżeli nasza hierarchia dziedziczenia będzie dość rozbudowana można zastosować inną strategię – joined strategy. Wykorzysta ona siłę współczesnych baz danych – relacje. W przypadku naszego kodu wystarczy, że zmienimy wartości w adnotacjach, aby stworzyć podstawową tabelę z bronią (klasa Weapon), zaś dodatkowe informacje przechowywane w każdej z klas zostaną zapisane w oddzielnych tabelach.

@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="WEAPON_TYPE",discriminatorType=DiscriminatorType.STRING)
public abstract class Weapon { ... }

I ponowne uruchomienie naszego kodu spowoduje utworzenie 3 różnych tabel:

ZrzutBroni

O dziwo nie zostały wygenerowane kolumny rozróżniające, ale taki kod działa:

 entityManager.getTransaction().begin();                

 RangeWeapon bowNew = (RangeWeapon) entityManager.find(Weapon.class,2L);
 System.out.println("Range attack: " + bowNew.getRangeAttack() );
 System.out.println("Name: " + bowNew.getName());

 entityManager.getTransaction().commit();

W konsoli (po uprzednim wyczyszczeniu persistence-context) generowane jest takie zapytanie SQL:

select weapon0_.id as id1_10_0_, weapon0_.name as name2_10_0_, weapon0_1_.rangeAttack as rangeAtt1_8_0_, weapon0_2_.stingAttack as stingAtt1_9_0_, case when weapon0_1_.id is not null then 1 when weapon0_2_.id is not null then 2 when weapon0_.id is not null then 0 end as clazz_0_ from Weapon weapon0_ left outer join RangeWeapon weapon0_1_ on weapon0_.id=weapon0_1_.id left outer join StingWeapon weapon0_2_ on weapon0_.id=weapon0_2_.id where weapon0_.id=?

Co wiele mówi o wydajności zapytań w przypadku używania tego typu strategii. Jednakże jeśli łatwość odwzorowania modelu oraz porządek logiczny w bazie danych jest dość istotny wówczas jest to rozwiązanie warte rozważenia.

Table-per-Concrete-Class Strategy

Jest to strategia, która niekoniecznie musi być zaimplementowana przez naszego dostawcę. Jej znaczenie jest w sumie proste – każda klasa w hierarchii dziedziczenia otrzymuje swoją własną tabelę. Po zmianie wartości adnotacji @Inheritance na InheritanceType.TABLE_PER_CLASS nasz kod po uruchomieniu spowoduje wygenerowanie ponownie 3 tabel, ale wyglądających tak:

ZrzutBroni

Zatem w bazie będą występowały informacje w sposób zdublowany. Jednakże rozwiązanie takie jest o wiele szybsze niż strategia relacyjna – zapytanie wybierające rekordy jest kierowane od razu do konkretnej tabeli, z pominięciem złączeń. Zatem o ile miejsce zajmowane przez dane nie jest istotne, albo też dane w bazie są prawie niezmienne, zaś często odpytywane warto zwrócić uwagę na tę strategię.

Próbując odpowiedzieć sobie na pytanie, która strategia jest ‘najlepsza’, odpowiedź będzie prosta – to zależy. Dość ciekawą dyskusję w tym temacie można znaleźć w wątku na GoldenLine.

Zaawansowane zagadnienia ORM – nazewnictwo, złożone klasy wewnętrzne i wielopolowe klucze główne

W przygotowaniach do certyfikacji OCEJPA zbliżamy się powoli do końca. Dzisiaj rozpocznę pierwszą część bardziej zaawansowanych tematów ORM. Ostatni wpis będzie dotyczył dziedziczenia w JPA i odwzorowywania tego w bazie danych.

Nazewnictwo tabel i kolumn

Do tej pory w naszych przykładach najczęściej grzecznie godziliśmy się na domyślne zachowanie JPA, które nazwę tabeli bezpośrednio kojarzyło z nazwą klasy encji. Zazwyczaj bazy danych nie robią z tym problemów – czyli nazwy kolumn i tabel są case-insensitive. Jednakże w zależności od bazy można czasem wymusić case-sensitivity. Co wtedy jeśli z bazy korzystamy z pomocą JPA? Jest to dość proste – korzystając z adnotacji @Table można wymusić użycie cudzysłowów w nazwie, a tym samym (jeśli dostawca bazy danych to obsługuje) korzystać z tabeli rozróżniając małe i wielkie litery. Wygląda to tak:

@Table(name="\"Dragon\"")

Oczywiście jeżeli pracujemy z bazą skonstruowaną w ten sposób wówczas adnotowanie w ten sposób wszystkich encji (lub też kolumn za pomocą adnotacji @Column) nie jest zbyt wygodne. Z pomocą może przyjść nam element konfiguracji JPA w XMLu o nazwie delimited-identifiers. Dzięki temu elementowi wszystkie wywołania SQL nazw tabel i kolumn zostaną opakowane w cudzysłowy.

Złożone obiekty wbudowane

O obiektach wbudowanych pisałem w jednym z wcześniejszych wpisów. W prosty sposób udało się nam opakować stan posiadania naszego bohatera w oddzielny obiekt, jednocześnie przechowując informację o tym w tej samej tabeli bazy daynych. Od wersji 2 specyfikacji JPA obiekty wbudowane posiadają też szereg innych możliwości – mogą zawierać w sobie inne obiekty, posiadać kolekcje oraz posiadać relacje do innych obiektów. Wszystko to jest możliwe wyłącznie przy założeniu, że to posiadająca encja jest podmiotem wszystkich tego typu relacji. Encja wbudowana nigdy nie występuje w oderwaniu od swojej encji posiadającej!

By sensownie pokazać działający kod musimy wrócić do przykładu ze stanem posiadania naszego bohatera. Wbudowana encja wygląda obecnie tak:

@Embeddable
public class FinanceState {
	
	private int cooper;
	private int silver;
	private int gold;
	
	// Gettery i settery uciete dla czytelnosci	
}

Zaś własność w encji bohatera wyglądała tak:

@Embedded    
private FinanceState finance;

Nasz obiekt wbudowany jest póki co dość ubogi – nie bardzo widać potencjał do rozszerzania go o relacje. Jednakże słusznie nazwaliśmy go dawno temu FinanceState – nasz bohater przecież może posiadać nie tylko pieniądze. Co z klejnotami czy posiadłościami? Bez problemu możemy wrzucić tego typu informacje do obiektu przechowującego stan posiadania. By zatem dać pokaz możliwości dodamy kolejną encję – Estate (reprezentującą posiadłość ziemską, a co, bohaterowie to nie zawsze przymierający głodem wiedźmini 😉 ).

@Entity
public class Estate {

    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;    
    
    private String name;
    private String location;

    // Gettery i settery pominiete dla czytelnosci
}

Teraz naszą klasę z informacjami o stanie posiadania możemy poszerzyć o taką deklarację:

@ElementCollection        
private Set<Estate> estates;

I jesteśmy w domu. Przy uruchomieniu kodu w bazie danych pojawią się tabele Estate oraz tabela łącząca o nazwie Heroes_Estate (nie zmienialiśmy ustawień domyślnych). Tak jak pisałem encje tego typu mogą również posiadać relacje do innych encji. Jednakże używany przeze mnie model encji niespecjalnie może coś takiego pokazać, zatem by nie płodzić miliona klas by pokazać prosty koncept podam link do przykładowego kodu (zresztą cały materiał jest wart przeczytania).

Wielopolowe klucze główne

W idealnym świecie encje są zawsze jednoznacznie identyfikowane przez klucz główny będący kolumną liczbową. Niestety czasami zdarza się (głównie w starszych bazach, choć takie rozwiązanie może mieć uzasadnienie biznesowe), że klucz główny składa się z kilku kolumn. W przypadku JPA implementacji takiego rozwiązania możemy dokonać na dwa różne sposoby – obiektu wbudowanego reprezentującego klucz główny, albo też zwykłej klasy reprezentującej klucz główny.

Zaczniemy od wbudowanej klasy klucza głównego, gdyż moim zdaniem jest to prostsze rozwiązanie. Klasa taka nie różni się absolutnie niczym od zwykłej klasy wbudowanej, istotne jest tylko to, że by występować w roli reprezentanta klucza głównego, musi ona implementować metody hashCode() oraz equals(), aby dostawca JPA mógł jednoznacznie porównywać instancje między sobą. Klasa taka (zresztą zwykła klasa zewnętrzna, o której napiszę później też) musi posiadać bezparametrowy konstruktor, być serializowalną, a także nie powinna mieć metod ustawiających (bo co to za klucz główny, który można zmienić). Załóżmy zatem, że chcielibyśmy naszego bohatera identyfikować nie za pomocą zwykłej liczby, ale kombinacji jego imienia i daty narodzin. Wbudowana klasa wyglądałaby tak:

@Embeddable
public class HeroId implements Serializable {
	
    private String name;
    private Date creationDate;

    public HeroId() {}
            
    public HeroId(String name, Date creationDate) {
        this.name = name;
        this.creationDate = creationDate;
    }

    public String getName() {
        return name;
    }

    public Date getCreationDate() {
        return creationDate;
    }      
   
}

Tym samym musimy zmodyfikować klasę Hero – na sam początek musimy usunąć istniejące w niej pola z imieniem i datą kreacji. Występowanie podwójnych pól spowoduje błąd kompilacji. Dodajemy też nowy konstruktor, a także adnotujemy pole identyfikatora. Ogólnie zmiany wyglądają tak:

@EmbeddedId private HeroId id;

public Hero( String name, Date creationDate ) {
        this.id = new HeroId(name,creationDate);
}

I tyle. Uruchomienie kodu nie zmieni nic w strukturze bazy danych – jedyne co ulegnie zmianie to sposób odwoływania się do obiektu (np. jego wyszukiwanie). Metoda find() EntityManagera pobiera poza typem encji klucz główny. Jednakże deklaracja metody zakłada, że identyfikator jest typu Object, zatem bez problemu możemy przekazać odpowiedni obiekt identyfikujący.

Drugim podejściem jest zwykła klasa klucza głównego. Zgodnie z nazwą jest to zwykła klasa, która musi spełniać te same warunki co klasa wbudowana (serializowalność, konstruktor, equals(), itp). Podstawową różnicą w stosunku do klasy wbudowanej jest to, że pola, na których oparty jest klucz pozostają wciąż w głównej encji, ale występują też w klasie identyfikującej.

Wracając do naszego bohatera klasa klucza głównego wygląda tak:

public class HeroId implements Serializable {
	
    private String name;
    private Date creationDate;

    public HeroId() {}
            
    public HeroId(String name, Date creationDate) {
        this.name = name;
        this.creationDate = creationDate;
    }

    // Gettery i equals/hashCode pominiete     
   
}

Zaś w samej encji bohatera przywracamy pola z imieniem i datą kreacji, ale adnotujemy je za pomocą adnotacji @Id, zaś samą klasę adnotujemy z użyciem @IdClass.

@Entity
@Table( name = "HEROES" )
@IdClass(HeroId.class)
public class Hero {

@Id
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
    
@Id
private String name;

// Reszta obcieta dla czytelnosci

}

I tyle. Wyszukiwanie encji odbywa się dokładnie tak samo jak w przypadku klasy wbudowanej. Moim zdaniem ten drugi sposób jest trochę bardziej czytelny – patrząc na kod klasy widzimy co jest kluczem, ale też jakie potencjalnie własności posiada klasa. Używanie klasy wbudowanej ma więcej sensu, jeśli własności, które składają się na klucz główny są ‘techniczne’ i raczej nie są nigdzie prezentowane.

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

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.

Blokady i cache

W procesie przygotowania do certyfikacji przyszedł czas na trochę bardziej skomplikowane zagadnienia. W dzisiejszym wpisie skupię się na dwóch z nich – blokowaniu oraz cachowaniu. Do roboty.

Podstawy

Blokowanie (ang. locking) jest procesem, który polega na zapewnieniu spójności zmian w bazie danych (w dużym uproszczeniu). Wyróżniamy dwa typy blokowania – optymistyczne oraz pesymistyczne. Ze względu na wydajność bazy danych najczęściej stosuje się blokowanie optymistyczne i od niego też zaczniemy. Podejście optymistyczne charakteryzuje się założeniem, iż obecnie wykonywana transakcja jest jedyną, która dokonuje zmian na naszej encji(ach). Pobieramy encję z bazy danych, wrzucamy do kontekstu, dokonujemy jakiś zmian i finalizujemy transakcję. W 99% takie podejście jest słuszne – przeprowadzone zmiany zostaną poprawnie odwzorowane w bazie danych. Co jednakże w przypadku, kiedy chwilkę przed commitem naszej transakcji, ktoś inny dokonał zmian w bazie? Ano wtedy przy próbie zapisu dostaniemy wyjątek OptimisticLockException, zaś transakcja zostanie cofnięta.

W jaki jednakże sposób nasz dostawca JPA wie, że stan w bazie danych się zmienił? Ano by to osiągnąć encja musi posiadać zadeklarowane pole/własność, w którym będzie trzymana informacja o wersji encji. Domyślnie w czystym JPA nie trzeba zadeklarować tego typu własność w encji – sam dostawca JPA dostarcza mechanizmy, które sprawdzają wersję encji, istnienie takiego pola nie jest wymagane. Jednakże jeśli z jakiegoś powodu chcielibyśmy mieć dostęp do wersji naszej encji możemy to zrobić dość prosto:

@Version private int version;  // oczywiscie nazwa moze byc dowolna

Do deklaracji takiego pola możemy użyć jednego z typów: int,short oraz long, ich wrapperów oraz java.sql.Timestamp. Wygląda to pięknie, jednakże jak zawsze jest łyżka dziegciu – nie zawsze można być pewnym, że własność ta została zaktualizowana w bazie danych w przypadku zmian dokonywanych poza JPA. Co więcej – istotnym jest też to, iż pole to niekoniecznie jest zmieniane w przypadku relacji, których encja nie jest właścicielem (stroną posiadającą). Jednakże da się to obejść – stosując jeden z typów blokowania optymistycznego.

Typy blokowania optymistycznego

Domyślnie JPA w przypadku używania pola wersjonującego używa dla transakcji izolacji Read Commited. Użycie tego typu oznacza to, iż dopóki transakcja nie zostanie zacommitowana, zmiany w niej wprowadzone nie będą widoczne dla kogokolwiek innego. Jednakże jeśli potrzebujemy bardziej zaawansowanych blokad, możemy wykorzystać dodatkowe możliwości JPA przy użyciu jednej z poniższych metod – trzech z metod interfejsu EntityManager oraz jednej interfejsu Query:

  • lock() – jednoznaczne zablokowanie wszystkich obiektów znajdujących się w danym persistence context
  • refresh() – jedna z sygnatur tej metody umożliwia przekazanie typu blokady do wskazywanej encji
  • find() – jedna z sygnatur tej metody umożliwia przekazanie typu blokady do pobieranej encji. Zwrócony wynik ma założoną blokadę.
  • Query.setLockMode() – pozwala na wskazanie typu blokady, która zostanie użyta podczas egzekucji zapytania.

Uzbrojeni w tę wiedzę możemy pokazać typy optymistycznego blokowania, które można użyć (wartości te są dostępne w typie wyliczeniowym LockModeType):

  • OPTIMISTIC – zachowanie, które najczęściej powoduje, że w momencie commitu transakcji baza danych wykonuje uaktualnienie encji za pomocą instrukcji SELECT ….. FOR UPDATE. Dzięki temu ewentualne zmiany dokonywane przez naszą transakcję spowoduję, że gdyby w danym momencie dane próbowała zaktualizować konkurencyjna transakcja – ta druga rzuci wyjątkiem. Jest to zachowanie bardzo bliskie domyślnemu zachowaniu JPA i dość często niczym się od niego nie różni.
  • OPTIMISTIC_FORCE_INCREMENT – robi dokładnie to samo co powyższa, z tym tylko, iż przy okazji nawet jeśli encja nie uległa zmianie (sama z siebie, ale np. zmieniliśmy jej relacje), numer wersji w bazie dla tej encji zostanie zmieniony.

Blokowanie pesymistyczne

Od JPA w wersji 2.0 mamy też możliwość stosowania blokowania pesymistycznego. W skrócie – przy pobieraniu encji z bazy danych następuje zablokowanie obiektu na poziomie bazy (blokada najczęściej dotyczy 1 wiersza), dzięki czemu inna transakcja nie może dokonywać jakichkolwiek zmian. Ma to jednakże dość spory narzut na wydajność oraz grozi potencjalnymi deadlockami.

Dobrze jest w tym miejscu również wspomnieć o tym, iż blokowanie optymistyczne ma miejsce zawsze w przypadku zadeklarowania pola @Version! Zatem nawet jeśli zablokujemy obiekt pesymistycznie, ale dopiero po jakimś czasie od pobrania encji z bazy, istnieje możliwość, że przy próbie zapisu dostaniemy OptimisticLockException. Dobrze jest to mieć na uwadze.

Jednakże czasem użycie tego typu blokowania jest konieczne – wówczas JPA dostarcza nam trzy typy blokowania.

  • PESSIMISTIC_WRITE – wspominałem, że w przypadku blokowania optymistycznego dostawca JPA może użyć ‘pod spodem’ instrukcji SELECT … FOR UPDATE. W przypadku tego typu na 99,9% tak właśnie zrobi.
  • PESSIMISTIC_READ – akurat ten typ niekoniecznie musi u konkretnego dostawcy bazy danych różnić w implementacji (chodzi o wywoływanie pod spodem SELECT … FOR UPDATE). Teoretycznie przy tym typie blokady encja jest blokowana zaraz po jej odczytaniu. Generalnie polecam lekturę wątku na Stackoverflow by zrozumieć różnice między tymi dwoma.
  • PESSIMISTIC_FORCE_INCREMENT – podobnie jak w przypadku wersji optymistycznej nawet jeśli encja nie zostanie zmieniona, wymuszamy zmianę jej wersji.

Na sam koniec warto wspomnieć o możliwych timeoutach dla oczekiwania na blokadę pesymistyczną. Większość dostawców JPA umożliwia wskazanie okresu czasu, po którym zostanie wyrzucony LockTimeoutException – w użyciu oczywiście jest znów mechanizm sugestii (ang. hints), gdzie nazwa sugestii to javax.persistence.lock.timeout (podawany w milisekundach). Generalnie wymieniony powyżej wyjątek otrzymamy nie tylko przy timeoucie, ale również przy innych błędach, które baza danych uzna za niekrytyczne. Jednakże w przypadku poważniejszych błędów (czyli takich, które mają docelowo spowodować rollback transakcji) otrzymamy PessimisticLockException

Cachowanie

Zasadniczo cachowanie jest mechanizmem dość dobrze znanym programistom. Jednakże w przypadku JPA mechanizmy te nie do końca są tym, do czego jesteśmy przyzwyczajeni. Dla przykładu – w rozwiązaniach komercyjnych (Memcached, EhCache, Oracle Coherence) to programista wyraźnie ‘wkłada’ dane do cache używając określonego klucza. Pobiera te dane również posiłkując się owym kluczem. W przypadku JPA jest trochę inaczej – programista bezpośrednio nie użytkuje cache. Ba! Specyfikacja JPA nie wymusza na dostawcy dostarczeniu w ogóle implementacji mechanizmu cache!

Co zatem w ogóle mamy na myśli pisząc – cache – po stronie JPA? Ano w tym kontekście za cache uznajemy współdzielony cache, z którego korzysta instancja EntityManager, a tym samym – persistence context. Dla programisty odbywa się to zupełnie transparentnie, zaś samo operowanie na cache jest realizowane przez dość ubogi interfejs o nazwie Cache. Odpowiednią implementację otrzymujemy poprzez wywołanie metody EntityManagerFactory.getCache(). Interfejs udostępnia kilka prostych metod – głównie chodzi o evict (umożliwia usunięcie konkretnej encji z cache) oraz contains (nazwa dość obrazowa). To wszystko. Samo pobieranie encji do cache i zarządzanie wersjami jest robotą dla dostawcy JPA i programista nie za wiele może tutaj zdziałać.

Co zatem możemy zrobić jeśli chodzi o cachowanie w JPA? Ano tak naprawdę to skupić się na dwóch rzeczach – co chcemy cachować, a także w jaki sposób to robić.
Do konfiguracji cachowania możemy użyć zarówno konfiguracji opartej o XML (persistence.xml), albo też adnotacji używanej w klasach encyjnych. Zapis w pliku konfiguracyjnym (przy pomocy elementu shared-cache-mode) docelowo ma zastosowanie w całej jednostce persystencji. Zanim przejdziemy do adnotacji wypada wylistować dostępne wartości:

  • NOT_SPECIFIED – wartość domyślna, która pozostawia dostawcy JPA decyzję czy cache jest w ogóle używany.
  • ALL – wszystkie encje są cachowane
  • NONE – odwrotnie do powyższego – nic nie jest cachowane
  • DISABLE_SELECTIVE – domyślnie cachowaniu podlegają wszystkie encje, z wyjątkiem tych, które oznacyzmy adnotacją @Cacheable(false)
  • ENABLE_SELECTIVE – przeciwieństwo poprzedniego – nie cachujemy niczego z wyjątkiem encji oznaczonych adnotacją @Cacheable(true)

To podstawowa konfiguracja zapisana w jednostce persystencji. Jednakże istnieje możliwość dynamicznego (w trakcie wykonywania programu) dostosowania konfiguracji. Istnieją dwie własności, które możemy ustawić – CacheRetrieveMode oraz CacheStoreMode. Wartości tych typów wyliczeniowych możemy przekazać do metody find() EntityManagera, albo też w formie podpowiedzi do instancji Query. Wypadałoby wytłumaczyć dlaczego mamy dwie oddzielne własności. Otóż jeżeli poruszamy się zawsze w obrębie jednej maszyny wirtualnej i jednego EntityManagerFactory problem zasadniczo nie istnieje. Jednakże w momencie, gdy dane w bazie danych mogą być zmieniane poza naszą aplikacją, wówczas rozróżnienie zapisu i odczytywania z cache staje się całkiem zrozumiałe. Wartości wspomnianych własności to – BYPASS, USE oraz w przypadku zapisu – REFRESH. Po szczegóły jednakże odsyłam do dokumentacji.

Metamodel i typowane zapytania criteria API

W poprzednim wpisie długo rozpisywałem się na temat Criteria API. Dość ciekawą konstrukcją w API było określanie typu pobieranych danych w przypadku wyciągania pojedynczych kolumn. Wyglądało to tak:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<Hero> hh = query.from(Hero.class);
query.select(hh.<String>get("name"))
     .where(cb.equal(hh.get("id"), 1L));

Jest to jednak podejście potencjalnie błędogenne. Możemy zawsze pomylić się w podawaniu typu kolumny, a dwa – przy zmianie encji i typu danych trzeba uaktualniać wszystkie zapytania oparte o kryteria. Nie do końca to fajne. Dlatego też w JPA istnieje coś fajnego – metamodel.

API Metamodelu

Zacznijmy od odpowiedzi na pytanie – co to jest ów metamodel? W skrócie są to informacje o wszystkich encjach w danym persistence-unit – o ich własnościach, typach czy relacjach. Zapisane w formacie obiektowym, dzięki czemu można na owym metamodelu działać cuda w trakcie działania aplikacji. Punktem wejścia do poznawania metamodelu jest nasza instancja EntityManagera, która posiada szereg metod, które zwracają odpowiednie informacje o encji, klasie wbudowanej czy typach (po kolei metody entity(), embeddable() i managedType().

Metamodel mm = entityManager.getMetamodel();
EntityType<Hero> hero_ = mm.entity(Hero.class);

Jak zawsze zachęcam do zapoznania się z dokumentacją dotyczącą Metamodel,EntityType, ManagedType oraz EmbeddableType. Cała hierarchia metamodelu jest dość rozbudowana i mocno ‘zgeneryzowana’. Dzięki temu jednakże możemy w trakcie działania aplikacji przyglądać się naszym encjom czy typom operując na mocnym typowaniu. By zaś konkretnie pokazać do czego przydaje się metamodel pierwszy kawałek kodu:

Metamodel mm = entityManager.getMetamodel();
EntityType<Hero> hero_ = mm.entity(Hero.class);
// Suffix na koncu sluzy informacji, ze nie jest to instancja encji per se - jest to stala konwencja w JPA
                
for( Attribute<? super Hero, ?> attr : hero_.getAttributes() ) {
      System.out.println( attr.getName() + " " + 
                          attr.getJavaType().getName() + " " + 
                          attr.getPersistentAttributeType() );
}

Rezultat w konsoli wygląda tak:

weapons java.util.List ONE_TO_MANY
id java.lang.Long BASIC
dragon com.wordpress.chlebik.jpa.domain.Dragon ONE_TO_ONE
deities java.util.List MANY_TO_MANY
finance com.wordpress.chlebik.jpa.domain.FinanceState EMBEDDED
level java.lang.Integer BASIC
name java.lang.String BASIC
creationDate java.util.Date BASIC
nicknames java.util.List ELEMENT_COLLECTION

Na tym przykładzie widać już do czego może się przydać metamodel w przypadku kryteriów – bez najmniejszych problemów możemy sprawdzić typ konkretnej własności. Nie obchodzi nas jaki on naprawdę jest – wystarczy, że mamy możliwość dostarczenia tej informacji przy konstruowaniu zapytania. Jednakże zanim uda się nam skompilować kod zawierający zapytanie z użyciem kryteriów (i silnego typowania) musimy zrobić jeszcze jedną rzecz – wygenerować ów metamodel.

Generowanie metamodelu

Domyślnie (przynajmniej w moim projekcie) metamodel nie jest generowany automatycznie. Istnieje możliwość wygenerowania go ręcznie – po prostu tworząc odpowiednią klasę z tym samym pakiecie co encja/typ/klasa wbudowana, której metamodel jest nam potrzebny. Czyli nasza klasa Hero miałaby metamodel, który wyglądałby tak:

package com.wordpress.chlebik.jpa.domain;

import java.util.Date;
import javax.annotation.Generated;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@StaticMetamodel(Hero.class)
public abstract class Hero_ {

	public static volatile SingularAttribute<Hero, Long> id;
	public static volatile SingularAttribute<Hero, Date> creationDate;
	public static volatile ListAttribute<Hero, Deity> deities;
	public static volatile ListAttribute<Hero, Nickname> nicknames;
	public static volatile SingularAttribute<Hero, Integer> level;
	public static volatile SingularAttribute<Hero, String> name;
	public static volatile SingularAttribute<Hero, Dragon> dragon;
	public static volatile ListAttribute<Hero, Weapon> weapons;
	public static volatile SingularAttribute<Hero, FinanceState> finance;

}

Tak jak pisałem – klasa powinna znajdować się w tym samym pakiecie co klasa docelowa, a także być oznaczona adnotacją @StaticMetamodel. Oczywiście mądrzy (i leniwi, zwłaszcza leniwi) programiści wymyślili automaty do generowania tego typu rzeczy. Najsensowniej rozpocząć od tego wpisu na Stackoverflow, a następnie zapoznać się z linkiem tam zamieszczonym, który jasno pokaże jak się uporać z tym problemem w zależności od używanego dostawcy JPA lub mechanizmu budowania projektu. W moim projekcie (generowany z użyciem Netbeans) wystarczyło dodać plik JAR z generatorem metamodelu Hibernate do classpath projektu i magicznie zaczęło działać. Podczas budowania projektu klasy metamodelu generowane są dynamicznie. Oczywiście należy wygenerowane klasy przerzucić do docelowego pakietu, aby IDE nie krzyczało i by kompilacja w ogóle się powiodła. Można robić to ręcznie, albo dopisać w narzędziu budowania projektu odpowiednią regułkę (wtedy jakiekolwiek zmiany w encjach czy typach są na 100% od razu odwzorowywane w metamodelu).

Dzięki powyższemu możemy ostatecznie przepisać nasze zapytanie do postaci unikającej konieczności umieszczania informacji o pobieranym typie. Taka forma użycia metamodelu nazywana jest ‘kanoniczną’.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<Hero> hh = query.from(Hero.class);                
query.select( hh.get( Hero_.name ) ) 
     .where(cb.equal(hh.get("id"), 1L));
TypedQuery<String> tq = entityManager.createQuery(query);

System.out.println("Imie bohatera: " + tq.getSingleResult()  );  

W sumie to tyle. Temat króciutki, ale dość ułatwiający życie programistom używającym kryteriów. Następnym razem zajmiemy się czymś jeszcze fajniejszym – cachowaniem i blokadami.

Dajcie kawałek SQLa – zapytania criteria API

W poprzednim wpisie zajmowaliśmy się JPQL. Możliwość tworzenia zapytań podobnych składniowo do SQL, ale z użyciem notacji obiektowej z całą pewnością upraszcza programowanie. Dzisiaj omówię rozwiązanie programistyczne – czyli tworzenie zapytań za pomocą notacji stricte obiektowej z użyciem odpowiednich metod – criteria queries.

Podstawy

Standardowo rozpoczniemy najprostszym możliwym przykładem. Oto wyjściowe zapytanie JPQL:


String jpql = "SELECT h.name FROM Hero h WHERE h.id = :id";
Query query = entityManager.createQuery(jpql, String.class)
                           .setParameter("id",1L);

System.out.println("Imie bohatera: " + query.getSingleResult()  );

Oczywiście jest to dość proste zapytanie, które jednakże posiada dość istotną wadę. Otóż o ile możemy użyć w nim parametrów, o tyle (np. w przypadku NamedQuery) musimy je określić przed uruchomieniem programu. Co w sytuacji kiedy wykonywane zapytanie zależy od danych dostarczonych przez użytkownika aplikacji? Oczywiście możemy sklejać zapytania JPQL bazując na przekazanych danych, ale ani to czytelne, a i o błąd dość łatwo. Właśnie dla takich sytuacji jak najbardziej przydają się Criteria Queries.

By rozpocząć od kodu – oto powyższe zapytanie przepisane z użyciem kryteriów:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);

Root<Hero> hh = query.from(Hero.class);
query.select(hh)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);
System.out.println("Imie bohatera: " + tq.getSingleResult().getName()  );

Kodu jakby więcej, ale jest on o wiele bardziej elastyczny. Podstawowym elementem wykorzystywania kryteriów jest interfejs CriteriaBuilder pobierany z instancji Entity Managera. Jest to punkt wejścia dla używania kryteriów. Dzięki metodzie tego interfejsu o nazwie createQuery tworzymy obiekt typu CriteriaQuery. To ten obiekt będzie reprezentował wszystkie warunki, które zamierzamy użyć w zapytaniu. Troszeczkę nie do końca oczywiste jest istnienie obiektu typu Root. Interfejs Root jest odpowiednikiem zmiennej wskaźnikowej w zapytaniach JPQL. Czyli jeśli w zapytaniu JPQL zastosujemy taką konstrukcję:


SELECT h FROM Hero h

To dokładnym odpowiednikiem tej konstrukcji w kryteriach jest właśnie:


Root<Hero> hh = query.from(Hero.class);

Na chwilę obecną wystarczy takie opisanie tego interfejsu. Dalej w kodzie odwołujemy się do obiektu reprezentującego nasze criteria query używając metod tego interfejsu jak i obiektu CriteriaBuilder. Nazwy metod są dość czytelne i póki co nie ma potrzeby ich wyjaśniać. Następnie wykorzystując nasz obiekt kryteriów tworzymy znaną już nam instancję TypedQuery i wywołujemy metody pobierające wyniki. Dlaczego w ten sposób? Przypomnijmy sobie, że w przypadku JPQL schemat działania jest taki sam, z tym tylko, że jako parametr przy tworzeniu obiektu zapytania służy łańcuch tekstowy z zapytaniem, a nie obiekt. Zmniejsza to powiązania między obiektami – obiekt kryteriów niesie informację co ma być pobrane z bazy, zaś obiekt TypedQuery wie w jaki sposób wykonać owo zapytanie. W tym miejscu warto jeszcze dodać, iż powyższy kod może zostać zapisany bez używania generyków, jednakże wówczas nie mamy możliwości sprawdzenia poprawności niektórych wywołań na etapie kompilacji. Ja preferuję używanie generyków gdzie to tylko możliwe.

Podstawy tworzenia zapytań

Jak widzieliśmy w powyższym przykładzie cała zabawa z kryteriami rozpoczyna się od interfejsu CriteriaBuilder. Posiada on trzy metody, których możemy użyć do stworzenia instancji reprezentującej interfejs CriteriaQuery:

  • createQuery(Class) – tworzy instancję generyczną, najczęściej używana, co zresztą zademonstrowałem na poprzednim listingu
  • createQuery() – to samo co powyżej, ale w wersji zwracającej Object
  • createTupleQuery() – metoda będąca tak naprawdę wywołaniem metody createQuery(Tuple.class). O tym czym jest Tuple (krotka) napiszę później

Kiedy posiadamy już obiekt CriteriaQuery możemy przystąpić do definiowania naszego zapytania. Podstawowe elementy języka SQL oraz tym samym JPQL jak słowa kluczowe SELECT, WHERE czy FROM mają swoje odpowiedniki w postaci metod obiektu CriteriaQuery. Jednakże zanim do nich przejdziemy zajmiemy się dość istotnym elementem – rdzeniem (root) zapytania. W przypadku JPQL mieliśmy do czynienia ze zmienną aliasu. Dzięki niej mogliśmy odwoływać się do kolejnych wartości encji, na której pracowaliśmy. W przypadku kryteriów również musimy stworzyć uchwyt, za pomocą którego będziemy mogli ‘dostać się’ do danych. W poprzednim przykładzie służyła do tego następująca konstrukcja:


Root<Hero> hh = query.from(Hero.class);

 

Posługujemy się tutaj interfejsem Root – zachęcam do zapoznania się z rodzicami tego interfejsu. Dzięki temu dość łatwo zrozumieć, co jest wykonalne za pomocą tegoż interfejsu (wrócę do tego). Na razie trzeba zaznaczyć tylko, iż w zapytaniu możemy użyć kilkunastu tego typu obiektów (co dotyczy zwłaszcza złączeń). Drugim elementem, który można wyróżnić na początku poznawania kryteriów są path expressions. W skrócie – jest to ‘ścieżka’, za pomocą której dochodzimy do interesujących nas własności obiektu. W poniższym zapytaniu:


SELECT h FROM Hero WHERE h.id = :id

 

Tego typu wyrażeniem jest fragment h.id. Przekładając powyższe zapytanie na kryteria otrzymamy kod, który przedstawiłem na samym początku:


Root<Hero> hh = query.from(Hero.class);
query.select(hh)
     .where(cb.equal(hh.get("id"), 1L));

 

Fragment hh.get(“id”) to odpowiednik kropki w zapytaniach JPQL.

 

Klauzula SELECT

 

Standardowo do pobierania wyniku zapytania służy metoda select. Przyjmuje ona jako argument obiekt implementujący interfejs Selection. Do tej pory przekazywaliśmy do tej metody instancję o typie Root – w ten sposób informowaliśmy JPA, że interesuje nas encja jako wynik zapytania. Jednakże jeśli interesuje nas np. samo imię maga (czyli wartość łańcuchowa), wówczas musimy zastosować następującą konstrukcję:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<Hero> hh = query.from(Hero.class);
query.select(hh.<String>get("name"))
     .where(cb.equal(hh.get("id"), 1L));
TypedQuery<String> tq = entityManager.createQuery(query);

System.out.println("Imie bohatera: " + tq.getSingleResult()  );

Argument przekazywany do metody select musi być kompatybilny z typem zwracanym przez definicję zapytania (CriteriaQuery). Dostawca JPA nie jest w stanie po samej nazwie wyciąganego parametru rozpoznać jego typu, dlatego też musimy użyć parametryzacji.

Nadszedł czas by opisać wspomniane już krotki. Co to są krotki (tuple)? Krotka to w skrócie obiekt, który zawiera dane otrzymane z zapytania (reprezentuje wiersz danych). Dostęp do nich jest możliwy zarówno w notacji indeksowej (czyli podajemy po prostu indeks zwróconego wyniku jak w czystym JDBC), albo za pomocą nazwy kolumn (jeśli zaliasujemy je). Dzięki temu jeżeli potrzebujemy tylko wyciągnąć z bazy konkretne dane, bez narzutu tworzenia encji wówczas krotki są rozwiązaniem – łatwo się po nich iteruje, a do tego są wydajniejsze niż encje. Krótki kawałek kodu:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<Hero> hh = query.from(Hero.class);
query.select(cb.tuple( hh.get("id").alias("id"), hh.get("name").alias("name")  ) )
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Tuple> tq = entityManager.createQuery(query);

for (Tuple t : tq.getResultList()) {
   System.out.println(t.get("id") + " " + t.get(1)  );
}

Skąd krotki? Ano bo musimy znać je by w pełni omówić metodę multiselect interfejsu CriteriaQuery. Jak widać z ostatniego kawałka kodu stworzenie odpowiednich wyrażeń dla metody select jest trochę ‘długaśne’. Co więcej – jeżeli chcemy wyciągnąć kilkanaście różnych danych (bardzo często z różnych tabel), zapytanie potrafi się z lekka rozrosnąć. Istotne jest również, że przy wyciąganiu szeregu kolumn rezultat może mieć kilka postaci – obiektu anonimowej klasy tworzonej w miejscu użycia, krotki lub tablicy obiektów (w zależności od typu używamy w metodzie select przy tworzeniu argumentu jedną z metod – tuple, construct lub array). By oszczędzić pracy programiście mamy dostęp do metody multiselect, która po prostu przyjmuje listę wartości, które chcemy wyciągnąć.
Tym samym nasz poprzedni przykład z krotkami dałoby się zapisać następująco:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<Hero> hh = query.from(Hero.class);
query.multiselect( hh.get("id").alias("id"), hh.get("name").alias("name") )
     .where(cb.equal(hh.get("id"), 1L))

//Reszta kodu po staremu

 

Klauzula FROM

 

O metodzie from już wspomniałem, pokazałem także przykład kodu. Teraz zajmiemy się czymś bardziej skomplikowanym, a konkretniej – złączeniami. Do ich obsługi używamy metodę o nazwie (a jakże) – join. Jak zawsze sugeruję zajrzeć do dokumentacji interfejsu Join – wtedy też dość łatwo zrozumiemy dlaczego nie ma problemu z łączeniem wywołań tej metody. Dowiemy się również wówczas, że metoda ta zwraca obiekt sparametryzowany – gdzie musimy zamieścić informację o typie encji, która jest podstawą złączenia, a także encji docelowej. Zobaczmy na przykładzie:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Join<Hero,String> nicknamesJoin = hh.join("nicknames");
query.select(hh.alias("hero"))
     .distinct(true)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);

for (Hero hero : tq.getResultList()) {
     System.out.println("Id bohatera: " + hero.getId()  );
     for( Nickname ne : hero.getNicknames() ) {
            System.out.println("Nickname: " + ne.getNick() );
     }
}

Pisząc o złączeniach nie sposób też poruszyć dość fajnej możliwości jaką są fetch join, czyli zapytanie, które przy okazji wykonania (jako efekt uboczny) pobiera do persistence context encje nie będące bezpośrednio używane podczas dostępu do zbioru wynikowego. Oto przykład:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
hh.fetch("nicknames");
query.select(hh.alias("hero"))
     .distinct(true)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);
Hero backupHero = null;

for (Hero hero : tq.getResultList()) {
     System.out.println("Id bohatera: " + hero.getId()  );
     backupHero = hero;
}                               

for( Nickname n : backupHero.getNicknames() ) {
    System.out.println(n.getNick());
}   

Druga pętla for jest tylko dla celów demonstracyjnych – prawda jest taka, że bez niej oryginalne zapytanie też wyciąga już informacje o przydomkach i zapisuje je w persistence context.

 

Klauzula WHERE

 

Klauzula WHERE jest w kryteriach dostępna poprzez metodę where(). Pochodzi ona z interfejsu AbstractQuery i jak zawsze zachęcam do zapoznania się z dokumentacją. Można wywoływać tę metodę bezargumentowo, albo też z parametrami w postaci obiektów Predicate lub pojedynczego argumentu Expression. Należy pamiętać, że każde kolejne wywołanie metody where() powoduje nadpisanie uprzednio dodanych warunków!

By odpowiednio uformować klauzulę WHERE dostępnych jest wiele metod – generalnie nie ma sensu ich tu listować skoro zrobiło to już za mnie Oracle. W dalszej części wpisu zajmę się tylko zagadnieniami wymagającymi trochę więcej uwagi – zaczniemy od predykatów.

W przypadku używania metody where mamy możliwość użycia kilku argumentów typu Predicate (np. w formie tablicy stworzonej z listy, gdyż metoda przyjmuje jako parametr varargs ). Co jednak mamy logikę warunkową i wolelibyśmy mieć jeden obiekt do przekazania? Da się to załatwić za pomocą metody conjuction() interfejsu CriteriaBuilder


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Predicate criteria = cb.conjunction();
//
// Warunki oczywiscie sa przykladowe i sluza tylko ilustracji
//
if( true ) {
     ParameterExpression<Long> id = cb.parameter(Long.class, "id");
     criteria = cb.and(criteria, cb.equal(hh.get("id"), id ) );
}
if( true ) {
     ParameterExpression<String> imieMaga = cb.parameter(String.class, "name");
     criteria = cb.and(criteria, cb.like( hh.<String>get("name"), imieMaga ) );
}                                   

query.select(hh)
     .where(criteria);

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("id", 1L);
tq.setParameter("name", "Chlebik%");                             

for (Hero hero : tq.getResultList()) {
    System.out.println("Id bohatera: " + hero.getId()  );
}

W powyższym przykładzie mieliśmy też okazję skorzystać z parametrów. W przypadku criteria API istnieje też możliwość używania literałów (większość metod przyjmuje albo Expression albo literały). Gdzie jednakże nie jest to możliwe, wówczas można literał po prostu zawrapować za pomocą metody literal() – literał dla wartości NULL tworzymy za pomocą metody nullLiteral( Class clazz ). Wróćmy jednak do parametrów – w powyższym przykładzie zastosowaliśmy trochę dłuższą drogę. Stworzyliśmy obiekt ParameterExpression by dodać go potem do zapytania. Jeśli jednakże ta konkretna definicja parametru nie będzie już w kodzie używana, wówczas możemy zrobić to trochę bardziej elegancko:


query.select(hh)
     .where(cb.like(hh.<String>get("name"), cb.parameter(String.class, "name") ) );

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("name", "Chlebik%");                             

Oczywiście mamy też możliwość budowania podzapytań. Da się to osiągnąć poprzez metodę subquery() interfejsu AbstractQuery, zwracana przez tę metodę wartość to instancja Subquery, która zasadniczo niewiele różni się od CriteriaQuery. Dość gadania, napiszmy jakiś kod (tak, wiem, że przykład jest przekombinowany, ale taki mamy model danych 😉 )

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);

Subquery<Hero> heroesWithWeapons = query.subquery(Hero.class);
Root<Hero> heroesWithWeaponsRoot = heroesWithWeapons.from(Hero.class);
Join<Hero,Weapon> heroesWithWeaponsJoin = heroesWithWeaponsRoot.join("weapons");
heroesWithWeapons.select( heroesWithWeaponsRoot ).where( cb.equal(
                                                            heroesWithWeaponsJoin.get("name"),
                                                            cb.parameter( String.class, "weaponName" ) )
);

// Radze zwrocic uwage na konstrukcje uzyta w metodzie IN()
query.select(hh)
     .where( cb.in( hh ).value(heroesWithWeapons) );

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("weaponName", "Super-Duper-Miecz");                             

Klauzule sortujące i grupujące

Do tej pory skupialiśmy się na pobieraniu danych i warunkach tegoż pobierania. Teraz przyszła pora na to,co zazwyczaj znajduje się po klauzuli WHERE – czyli na instrukcje grupujące i sortujące. Sortowanie jako prostsze omówię na początek – służy do tego obiekt typu Order, który można otrzymać używając metod asc() lub desc() interfejsu CriteriaBuilder.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);

query.select(hh).orderBy( cb.desc(hh.get("level")) ) ;

Metody agregujące i grupujące są równie proste w użyciu – na tym przykładzie wybieramy bohaterów, którzy mają przynajmniej jedno bóstwo:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Join<Hero,Deity> deities = hh.join("deities");

query.select(hh)
     .groupBy( hh )
     .having( cb.ge( cb.count(deities), 1 ) );

TypedQuery<Hero> tq = entityManager.createQuery(query);

To tyle na dziś. W następnej notce napiszę coś więcej na temat silnie typowanych zapytań oraz metadanych.