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.

Dajcie kawałek SQLa czyli Java Persistence Query Language – Podstawy

Pamiętam pierwszą styczność z JPA i szczere zdziwienie jak fajnie może teraz być. Bez ręcznie klepanych SELECT * FROM. Jednakże jak to w życiu bywa z czasem okazało się, że o ile na co dzień używanie encji jest bardzo praktyczne, o tyle przy konieczności napisania jakiegoś typowo bazodanowego kodu (np. zmian w bazie danych wykonywanych cyklicznie na całym zbiorze danych) encje nie do końca są odpowiednim narzędziem. Autorzy JPA przewidzieli tego typu problemy zawczasu i stąd zaimplementowali możliwość używania zapytań. Jakich zapytań? Ano zapraszam do dalszej lektury.

Podstawy

Zaczniemy od operacji najniższego poziomu – chcesz używać czystego SQLa? Nie ma z tym problemu, da się, ale nie dziś o tym będzie mowa (na pewno o tym zagadnieniu napiszę w przyszłości). Dziś skupimy się na wynalazku o nazwie Java Persistence Query Language. Jest to technologia, którego początki leżą w mrocznych czasach EJB2. Od tamtego czasu rozwiązanie to trochę udoskonalono (delikatnie rzecz mówiąc). Dziś jest to sposób na pobieranie danych z bazy danych za pomocą składni przypominającej SQLa, ale niezależnej od dostawcy bazy danych oraz operującej na notacji obiektowej.

Niejasne? Nie dziwię się, kod więcej potrafi powiedzieć niż niejeden akapit tekstu.

SELECT h FROM Hero h

Co właśnie uczyniliśmy? Pobraliśmy listę wszystkich dostępnych bohaterów z tabeli. Dzięki zastosowaniu aliasu ‘h’ mamy punkt zaczepienia, z którego możemy iść dalej. Potrzebujemy tylko imion? Ależ proszę bardzo.

SELECT h.name FROM Hero h

Wykonując takie zapytanie otrzymamy listę tylko i wyłącznie imion herosów. Potrzebujemy imienia smoka, którego posiada nasz bohater? Problemu nie ma, za pomocą powyższej notacji możemy dostać się do dowolnej właśności encji.

SELECT h.dragon.name FROM Hero h

Myślę, że dość jasno już widać do czego może posłużyć nam JPQL. Jeżeli musimy pobrać dane posługując się trochę bardziej wyszukanymi kryteriami niż klucz główny mamy gotowe do zastosowania narzędzie.

Filtrowanie wyników i złączenia

Powyżej używaliśmy prostych zapytań, które pobierały wszystkie rekordy danego typu. Oczywiście możemy filtrować pobierane wyniki.

SELECT h FROM Hero h WHERE h.name = 'Chlebikowy Mag'

Wspomniałem, że używanie JPQL ma tę przewagę nad ‘wyciąganiem’ encji w tym, iż potrafi zredukować dość istotnie ilość pobieranych danych. Jeżeli chcemy wygenerować prostą tabelkę z danymi dotyczącymi encji nie jest koniecznym pobieranie jej oraz wszystkich jej zależności. Możemy osiągnąć cel w prostszy sposób:

SELECT h.name, h.level FROM Hero h 

Dzięki temu zwrócimy dane tekstowe bez konieczności obciążania bazy oraz łącza przesyłaniem niepotrzebnych nam danych. To jedna z największych zalet JPQL. Oczywiście jeśli potrzebujemy danych z innych encji będących w relacji z naszą (czyli po prostu na poziomie SQLa wykonujemy złączenie) również nie będzie problemu. Załóżmy, że poszukujemy bohatera, jednakże nie znamy jego imienia. Znamy za to imię jego smoka. Do dzieła.

SELECT h FROM Hero h, Dragon d WHERE d.name = 'Smok Wawelski'

Hibernate bez naszego udziału wygeneruje odpowiednie zapytania do bazy danych dzięki czemu będziemy mogli otrzymać poprawny rekord. Możemy skorzystać również z bardziej bezpośredniej składni.

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = 'Smok Wawelski'

Nie ma również problemu by skorzystać z oferowanych w SQL funkcji agregujących. Dla przykładu wyciągniemy liczbę wszystkich rekordów w tabeli.

SELECT COUNT(h) FROM Hero h

Dostępne są też standardowe funkcje jak MAX, MIN czy AVG. Istnieje również możliwość filtrowania wyników grupowania za pomocą instrukcji HAVING.

Powyższe przykłady pokazują tylko bardzo podstawową funkcjonalność JPQL. Póki co pokazałem podstawy, dzięki którym będzie można zaprezentować w jaki sposób używać JPQL z poziomu kodu. Dla niecierpliwych zaś polecam dokumentację JPQL na stronie Oracle, gdyż jej przepisywanie w formie krótkich kawałków kodu niespecjalnie ma sens. Zresztą sam JPQL pod kątem używanych słów kluczowych niespecjalnie różni się od czystego SQL.

Jak to uruchomić?

Jeżeli do tej pory zadawaliście sobie pytanie – wszystko ładnie, ale jak to uruchomić i przeprocesować wynik – oto nadeszła pora na odpowiedź. Wrzucanie kolejnych metod do Entity Managera by reprezentować obiekt zapytania nie byłoby sensowne (ileż to musiałoby być metod) – dlatego też stworzono dwa interfejsy – Query oraz TypedQuery (wprowadzony w JPA2). Wyróżniamy dwa typy zapytań – dynamiczne i predefiniowane (statyczne)

  • dynamiczne – jest tworzone w czasie działania programu
  • statyczne – jest definiowane w metadanych encji (za pomocą adnotacji lub XML), przewagą tego typu zapytań jest ich prekompilacja, a tym samym przyspieszenie wykonywania

By fizycznie wykonać zapytanie musimy wrócić do Entity Managera – posiada on metodę createQuery, której parametrami jest łańcuch tekstowy z zapytaniem (jeśli chcemy stworzyć instancję typu Query) oraz obiekt Class zwracanego wyniku (jeśli jesteśmy zainteresowani stworzeniem obiektu typu TypedQuery). Oto przykład:

String jpql = "SELECT h.name FROM Hero h WHERE h.id = 1";
TypedQuery query = entityManager.createQuery(jpql, String.class);
System.out.println("Imie bohatera: " + query.getSingleResult());

Wynikiem działania powyższego kodu jest:

Hibernate: select hero0_.name as col_0_0_ from HEROES hero0_ where hero0_.id=1
Imie bohatera: Chlebikowy Mag

Użyta metoda getSingleResult jest jedną z wielu zdefiniowanych przez interfejs Query. Polecam zapoznanie się z opisem tego interfejsu, gdyż przeklejanie dokumentacji niespecjalnie ma sens. Dla pełności wywodu zajmiemy się teraz zapytaniami statycznymi. Zaleca się ich stosowanie w sytuacjach, w których z góry wiadomo jaka będzie treść zapytania. Jeśli w naszej aplikacji będziemy dość często potrzebować listy bohaterów wraz z imionami ich smoków, wówczas jest to świetna okazja by wprowadzić zapytanie statyczne. We wszystkich wpisach operuję na adnotacjach zatem i tym razem nie będzie inaczej.

@NamedQuery(name="getAllHeroesNamesWithDragonNames",
            query="SELECT h.name, d.name FROM Hero h, Dragon d")
@Entity
public class Hero 

Jak widać podstawą jest adnotacja @NamedQuery. Atrybut query jest dość oczywisty, podobnie name. Jednakże w związku z tym ostatnim dobrze jest wspomnieć, iż nazwa jest rejestrowana w ramach danego persistence-unit. Dlatego też dobrze jest zapewniać ich unikalność by uniknąć ewentualnych konfliktów (np. poprzez stosowanie prefixów). Nie ma problemu by zdefiniować dla encji kilka zapytań, ale wtedy musimy posiłkować się adnotacją @NamedQueries, która przyjmuje tablicę z wartościami.

@NamedQueries({
      @NamedQuery(name="getAllHeroesNamesWithDragonNames",
                  query="SELECT h.name, d.name FROM Hero h, Dragon d"),
      @NamedQuery(name="getAllHeroesNamesWithLevelAndDragonNames",
                  query="SELECT h.name, h.level, d.name FROM Hero h, Dragon d")
})
@Entity
public class Hero 

W jaki sposób używać tego typu zapytań? Dość prosto:

 TypedQuery<Object[]> query = (TypedQuery<Object[]>) entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames");
 List<Object[]> res = query.getResultList();                
               
 for( Object[] o : res ) {
      System.out.println("Imie bohatera: " + o[0]  );
      System.out.println("Level bohatera: " + o[1]  );
      System.out.println("Imie smoka: " + o[2]  );
 }

Sam kod wyciągający wygląda dość brzydko (zauważ, że pobieramy wartości proste, a nie obiekty encji), ale reguła działania named-queries powinna być dość czytelna. Po raz kolejny zachęcam do zapoznania się z dokumentacją interfejsów zapytań – jest to kopalnia informacji.

No dobrze, a mogę dorzucić parametr?

Pewnie, że możesz. Co więcej – mamy znany choćby ze Springa mechanizm named-parameters, a także standardowe positional-parameters – tak jak w czystym JDBC. Jeden z poprzednich przykładów możemy zatem zapisać tak:

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = ?1

lub tak:

SELECT h FROM Hero h JOIN h.dragon d WHERE d.name = :dragonName

W jaki sposób podstawić wartości do odpowiednich parametrów? Znów musimy odwiedzić interfejs Query, który zawiera szereg metod setParameter (UWAGA! W przypadku użycia obiektów typu Date, Time lub Timestamp wymagany jest 3 parametr typu wyliczeniowego TemporalType ).

String jpql = "SELECT h FROM Hero h, Dragon d WHERE d.name = :dragonName ";                
                
Hero hh = entityManager.createQuery(jpql, Hero.class)
                       .setParameter("dragonName","Smok Wawelski")
                       .getSingleResult();
                
System.out.println("Imie bohatera: " + hh.getName()  );

Viola! Działa. Zadziałałoby również w przypadku zamiany :dragonName na ?1 oraz wywołania metody setParameter(“dragonName”,”Smok Wawelski”) na setParameter(1,”Smok Wawelski”). Z góry jednakże widać, że używanie named-parameters jest zdecydowanie bardziej czytelne.

Wykonywanie zapytań – co jeszcze?

Wróćmy do pobierania danych przez zapytanie. Jak widać było na powyższych listingach rezultatem zwracanym przez zapytanie JPQL mogą być:

  • typy proste – Stringi, prymitywy czy typy JDBC
  • encje
  • tablica obiektów
  • typy zdefiniowane przez użytkownika (nie encje)!

Z typami prostymi i tablicą obiektów mieliśmy już styczność, z encjami zasadniczo też. W przypadku encji istotnym jednakże jest udzielenie odpowiedzi na pytanie – w jakim stanie otrzymujemy ową encję? W przypadku pobieraniu danych za pomocą metody find Entity Manager dostajemy zarządzaną encję. Dokładnie to samo zachowanie będziemy mogli zaobserwować w przypadku rezultatu zapytania JPQL. Zwrócona encja jest zarządzalna i ewentualne zmiany tej encji w ramach wciąż istniejącej transakcji zostaną odwzorowane w bazie danych. Jedynym wyjątkiem jest transaction-scoped Entity Manager – jeżeli jest użyty poza transakcją wszystkie pobrane encje od razu są pobierane w stanie detached. Co prawda możemy je potem podpiąć do kontekstu metodą merge, ale należy o tym pamiętać! Z drugiej strony, zwracanie encji w stanie detached ma niezaprzeczalną zaletę – o wiele mniej obciąża pamięć. Jeśli zatem jedynym celem działania naszej metody jest pobranie wyników do ich zaprezentowania (tylko i wyłącznie), dobrze byłoby z góry zaplanować metodę pobierającą beana jako nie korzystającą z transakcji (za pomocą adnotacji @TransactionAtribute z wartością NOT_SUPPORTED).

Ostatnim punktem na liście powyżej są typy zdefiniowane przez użytkownika, ale nie będące encjami. Wróćmy do naszego przykładu pobierającego imiona bohaterów, ich poziomy oraz imiona smoków.

 TypedQuery<Object[]> query = (TypedQuery<Object[]>) entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames");
 List<Object[]> res = query.getResultList();                
               
 for( Object[] o : res ) {
      System.out.println("Imie bohatera: " + o[0]  );
      System.out.println("Level bohatera: " + o[1]  );
      System.out.println("Imie smoka: " + o[2]  );
 }

Operowanie po tablicach obiektów nie wygląda zbyt profesjonalnie. Dlatego też jeśli chcemy uczynić powyższy kod bardziej czytelnym możemy zwrócony przez zapytanie wynik opakować w specjalnie do tego celu stworzoną klasę (przypomina to trochę użycie RowMappera JDBC). Stosuje się do tego słowo kluczowe NEW w treści zapytania SELECT. Wpierw utwórzmy prostą klasę opakowującą:

public class HeroNameLevelDragonNameWrapper {
    
    private String heroName;
    private Integer level;
    private String dragonName;
    
    public HeroNameLevelDragonNameWrapper(String heroName, Integer level, String dragonName ) {
        this.dragonName = dragonName;
        this.heroName = heroName;
        this.level = level;
    }

    // GETTERY I SETTERY POMINIETE
}

Klasa już jest, teraz należy zmienić treść zapytania (zwróć uwagę na słówko NEW oraz podanie pełnej nazwy klasy).

@NamedQueries({     
      @NamedQuery(name="getAllHeroesNamesWithLevelAndDragonNames",
                  query="SELECT NEW com.wordpress.chlebik.jpa.domain.HeroNameLevelDragonNameWrapper(h.name, h.level, d.name) FROM Hero h, Dragon d")
})

Zaś w kodzie uruchomieniowym możemy teraz użyć takiej konstrukcji:

TypedQuery<HeroNameLevelDragonNameWrapper> query = entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames",HeroNameLevelDragonNameWrapper.class);
List<HeroNameLevelDragonNameWrapper> res = query.getResultList();                

for( HeroNameLevelDragonNameWrapper o : res ) {
    System.out.println("Imie bohatera: " + o.getHeroName()  );
    System.out.println("Level bohatera: " + o.getLevel()  );
    System.out.println("Imie smoka: " + o.getDragonName()  );
}

Co jest o wiele bardziej czytelne i zgodne z dobrymi praktykami.

Jak wspomniałem na początku wpisu – używanie JPQL pozwala na poprawienie wydajności wykonywanych zapytań. Pomijając fakt, iż nie musimy budować całego drzewa zależności pobieranych encji, bardzo dobrą praktyką jest również paginacja zwracanych wyników. Chyba każdy w swojej karierze zawodowej prędzej czy później był zmuszony zaimplementować jakąś formę paginacji wyników. Podstawowym problemem jest zawsze ilość danych, które pobieramy. najmniej wydajnym rozwiązaniem jest pobranie wszystkich wyników, a następnie ‘obrabianie ich’ programistycznie. Jest to o tyle nieefektywne, iż w 99% przypadków użytkownik nie musi przeglądać niczego poza pierwszą stroną. Obciążenie pamięci i bazy jest tym samym zupełnie nieefektywne.

Do sensownego nawigowania po dużych zbiorach danych interfejs Query (i jego potomkowie rzecz jasna) udostępnia metody setFirstResult oraz setMaxResults. Ta pierwsza ustawia punkt początkowy, od którego będziemy pobierać dane zwrócone przez zapytanie, zaś druga metoda określa ilość rekordów, którymi jesteśmy zainteresowani. Sposób implementacji zachowań tych metod zależy od dostawcy JPA dlatego dobrze jest zapoznać się z dokumentacją używanej biblioteki, aby mieć pewność poprawnego działania.

Podpowiedzi

twórcy JPA stanęli w pewnym momencie w tym samym miejscu, w którym prędzej czy później lądują twórcy specyfikacji czy standardów. Skoro tworzony jest nowy standard, a do tej pory mieliśmy kilkunastu dostawców podobnych rozwiązań, w jaki sensowny sposób spróbować zunifikować różnice między nimi? Istnieje również drugi problem – standard standardem, ale zazwyczaj twórcy implementacji dodają z chęcią coś od siebie (w sumie to jedna z głównych przyczyn różnic między dostawcami JPA). By ułatwić rozszerzanie funkcjonalności JPA bez konieczności zmiany API dodano możliwość dodawania do zapytań ‘podpowiedzi’ (ang. hint).

Podpowiedzi można ustawić dla zapytania dwojako – albo programistycznie wywołując metodę setHint, albo też poprzez metadane (w adnotacji NamedQuery). Pełna lista podpowiedzi jest zależna od dostawcy. W przypadku Hibernate istnieje do tego nawet specjalna klasa QueryHints, która zawiera stosowne wartości. Zerknijmy na przykład:

TypedQuery<HeroNameLevelDragonNameWrapper> query = entityManager.createNamedQuery("getAllHeroesNamesWithLevelAndDragonNames",HeroNameLevelDragonNameWrapper.class);
query.setHint(QueryHints.TIMEOUT_JPA, 5000L);
List<HeroNameLevelDragonNameWrapper> res = query.getResultList();                

W ten sposób możemy podpowiedzieć JPA szereg różnych rzeczy – jak choćby powyższy timeout zapytania, sposób pobrania wierszy czy dodanie komentarza do zapytania.

Operacje masowe

Tak zwane bulk-operations dotyczą wykonywania instrukcji UPDATE oraz DELETE na wierszach bazy danych. Mogą być one bardzo użyteczne, ale przy ich użyciu dobrze jest zrozumieć jedną zasadę – You are alone 🙂
Operacje tego typu wykonujemy następująco (tak, wiem że kod jest prosty):

String jpql = "UPDATE Hero h SET h.name = :newName WHERE h.id = :id";                
                
entityManager.createQuery(jpql)
             .setParameter("newName","Nowy lepszy Chlebikowy Mag")
             .setParameter("id",1L)
             .executeUpdate();                

Co jest problematycznego w tym zapytaniu? Ano to, że ewentualnie wcześniej pobrane do persistence-context dane niekoniecznie muszą zostać odświeżone! Jest to bardzo istotne, co zresztą pokaże ten kod i jego rezultat.

Hero hh = entityManager.find(Hero.class, 1L);
System.out.println("Pobranie bohatera");
String jpql = "UPDATE Hero h SET h.name = :newName WHERE h.id = :id";                                

entityManager.createQuery(jpql)
             .setParameter("newName","Nowy lepszy Chlebikowy Mag")
             .setParameter("id",1L)
             .executeUpdate();

System.out.println("Zapisanie zmiany bohatera");                                
System.out.println("Imie bohatera: " + hh.getName()  );

Oto konsola:

Pobranie bohatera
Hibernate: update HEROES set name=? where id=?
Zapisanie zmiany bohatera
Imie bohatera: Chlebikowy Mag

Zmiana nie została odwzorowana w persistence-context. Dlatego w przypadku transakcji zarządzanych przez kontener najlepiej jest zapewnić wykonywanie tego typu operacji (to samo jest z operacjami DELETE) w oddzielnej transakcji – pobierać zaś wyniki, które mogłyby być zmienione przez taką operację dopiero po jej wykonaniu.

Na sam koniec zaś odniosę się też do operacji DELETE – tworzone i wykonywane poprzez JPQL nie podlegają kaskadowości! Tym samym owszem – być może usuniemy wskazane encje, ale relacje opatrzone adnotacjami CascadeType.PERSIST pozostaną w bazie danych. Dlatego raz jeszcze – You are alone.

Entity Manager i okolice – część 2

Dziś będziemy kontynuować zgłębianie tajemnic Entity Managera. Konkretnie zaś zajmiemy się stanem encji – co się z nimi dzieje kiedy powstają i zaczynają być zarządzane, a co kiedy przy próbie zapisu otrzymamy wyjątek, a nasze encje zostaną odłączone (tzw. detach). Do dzieła.

Co się dzieje z moją encją?

Załóżmy, że tworzymy od podstaw bohatera. Kawałeczek kodu dla przypomnienia:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");

List&amp;amp;lt;Nickname&amp;amp;gt; nickNames = new ArrayList&amp;amp;lt;Nickname&amp;amp;gt;();
Nickname n1 = new Nickname(); n1.setNick("Ripper");
Nickname n2 = new Nickname(); n2.setNick("Butcher");
Nickname n3 = new Nickname(); n3.setNick("Killer");
nickNames.add(n1);
nickNames.add(n2);
nickNames.add(n3);
h.setNicknames(nickNames);

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 );

Po utworzeniu encji stają się one zarządzane (ang. managed ). W tym momencie wszystkie przynależą do peristence context, który jest strukturą rezydującą w pamięci. Wszelakie operacje na encjach są dokonywane w pamięci i dopiero kiedy następuje commit transakcji mamy pewność, że JPA próbuje zapisać zmiany w bazie danych. Proces ten określa się ładnym angielskim słowem – flush. Dokładnie tak samo nazywa się metoda Entity Managera, która robi dokładnie to samo – próbuje wykonać na bazie danych wszystkie operacje, które wykonano w ramach bieżącej transakcji. Zatem jeżeli uważamy, że w danym momencie istnieje konieczność zapisania w bazie zmian dokonanych na encjach możemy wymusić na JPA takie właśnie zachowanie. Oto zmodyfikowany przykład:

Dragon d = new Dragon();
d.setName("Smok Wawelski");				

entityManager.persist(d);
entityManager.flush();

System.out.println("Zapisanie smoka o ID: " + d.getId()  );

h.setDragon(d);
d.setRider(h);
entityManager.persist( h );

Co skutkuje takim wyjściem w logach:

Hibernate: select max(id) from Dragon
Hibernate: insert into Dragon (name, id) values (?, ?)
Zapisanie smoka o ID: 1
Hibernate: select max(id) from HEROES
Hibernate: insert into HEROES (creationDate, dragon_id, co, gl, si, level, name, id) values (?, ?, ?, ?, ?, ?, ?, ?)

Powyżej wspomniałem, że mamy pewność, iż JPA spróbuje zapisać zmiany w bazie danych przy commicie transakcji. Jednakże nikt nie twierdzi, że nie może nastąpić to kiedy indziej. Jeśli provider stwierdzi konieczność wykonania operacji flush to po prostu to uczyni. Oczywiście ze względów na koszt dostępu do bazy danych nie następuje to przy każdej możliwej okazji, ale dobrze jest wiedzieć, że jest to możliwe.

Za jakiś czas wrócę do momentu zapisu zmian w bazie danych podczas operacji flush. Tymczasem zajmę się opisaniem dość istotnego z punktu widzenia JPA zagadnienia jakim jest detachment. Co to takiego? Ano jest to stan, w którym może znaleźć się encja. W jaki sposób do tego dochodzi? Tak naprawdę jest na to wiele sposobów.

  • application-managed persistence context, który zarządzał encją został zamknięty
  • w przypadku transaction-scoped entity managerów commit transakcji powoduje, że wszystkie encje w ramach persistence context przechodzą do stanu detached (spróbujcie usunąć encję smoka uprzednio kończąc transakcję i wykorzystać do tego istniejący obiekt – nic się nie stanie)
  • jeśli statefull session bean operujący na rozszerzonej transakcji zostanie zniszczony to wszystkie zarządzane przez niego encje zmieniają stan na detached
  • jeśli podczas commitu transakcji wystąpi błąd, a następnie rollback wówczas wszystkie zmienione encje zostają odłączone
  • użycie metod clear lub detach Entity Managera
  • serializacja encji – zserializowana postać encji jest odłączana od persistence context

Jak zatem widać w dotychczasowym kodzie też mieliśmy do czynienia z odłączonymi encjami nawet o tym nie wiedząc 😉 Przypomnijcie sobie fakt usuwania smoka po jego uprzednim zapisaniu – nigdy nie używaliśmy istniejącego obiektu, a zawsze od nowa pobieraliśmy jego nową instancję za pomocą metody find.

Jednakże co się stanie z encją jeśli przy próbie zakończenia transakcji zostanie rzucony wyjątek? W tym momencie nasze encje są niejako odłączane od persistence context, co określa się ładnym słowem – detach. Czy taka encja może się nam do czegokolwiek jeszcze przydać? Ano okazuje się, że może.

Co bowiem oznacza dokładnie fakt, iż encja jest w stanie detached? Nie jest już zarządzana przez JPA, co oznacza, że nie może być argumentem dla metod, które do poprawnego działania wymagają zarządzanej encji. Z drugiej strony cały stan encji wciąż jest w niej obecny – czyli nie ma problemu by ‘wyciągnąć’ z niej interesujące nas dane. Załóżmy, że wracamy do naszego smoka – w poprzednim wpisie zajmowaliśmy się usuwaniem encji z bazy danych. Teraz zrobimy to ponownie:

h.setDragon(null);
entityManager.remove(d);

// commit transakcji i ew. wyczyszczenie kontekstu w przypadku application-managed Entity Managera

System.out.println("Dragon: " + d.getId() + " - " + d.getName() )

Logi są dość oczywiste:

Hibernate: update HEROES set creationDate=?, dragon_id=?, co=?, gl=?, si=?, level=?, name=? where id=?
Hibernate: delete from Dragon where id=?
Dragon: 1 – Smok Wawelski

Jak widać mimo usunięcia obiektu z bazy danych (instrukcja DELETE) obiekt w aplikacji dalej zawiera potrzebne nam dane. Oczywiście problem pojawi się w sytuacji, w której potrzebowalibyśmy informacji o danych, które podlegają lazy-loading. Jest to dość powszechna sytuacja w aplikacjach sieciowych – pobieramy dane z bazy za pomocą JPA, po czym wynik (dość często lista encji) jest przekazywana w formie modelu do warstwy widoku. Tam iterujemy po naszej liście encji jednocześnie próbując odwołać się do własności encji, która podlega lazy-loading (dla przykładu jest to relacja many-to-many). Co wtedy? Nie da się tego do końca przewidzieć – zależy to od dostawcy.

Zaradzić problemom można na 2 sposoby (przynajmniej na razie):

  • zmienić sposób pobierania relacji na EAGER. Co jednakże nie zawsze ma sens ze względów wydajnościowych.
  • po pobraniu encji należy w kodzie (serwletu, usługi) odwołać się do wszystkich relacji, które chcemy następnie wykorzystać w warstwie widoku. Wygląda to dziko w kodzie, ale przynajmniej działa.

Nie są to specjalnie wygodne i ‘czyste’ rozwiązania. Dlaczego zatem nie zabrać się za problem z troszkę innej strony? Może w ogóle uniknijmy odłączenia encji od persistence context? Dzięki temu niezależnie do jakich własności/relacji nie odwoływalibyśmy się w widoku nie będziemy mieli problemu z otrzymaniem danych. W tym celu transakcję obsługuje się na warstwie serwletu, który obsługuje całe żądanie. Jednakże rodzi to kwestie związane z faktem, iż w każdym serwlecie musimy posiadać logikę, która otwiera i zamyka transakcję. Do tego dochodzą też kwestie związane z tym, iż dość często warstwę kontrolera obsługujemy za pomocą frameworków webowych (np. Spring MVC), który posiada swoje bajerki transakcyjne (patrz: Transakcje Spring z Hibernate i dobre prkatyki springowego @Transactional ). Zdecydowanie lepiej jest ten problem obchodzić korzystając z predefiniowanych zapytań JPQL, ale o tym napiszę w kolejnych artykułach. Na sam koniec warto wspomnieć, że możemy sprawdzić czy encja jest zarządzana poprzez metodę contains Entity Managera, które parametrem jest encja, a wartością zwracaną prawda lub fałsz.

Ponowne zarządzanie encją

Odłączona od persistence contextu encja może zostać ponownie do niego dołączona i stać się ponownie częścią wielkiej zarządzanej rodziny 😉 By to osiągnąć musimy wykorzystać metodę Entity Managera o nazwie merge. Rozpatrzmy taki kod:


entityManager.getTransaction().begin();  

Hero h = new Hero();

// CIACH - tu caly poprzedni kod

entityManager.persist( h );
entityManager.getTransaction().commit();

entityManager.getTransaction().begin();
entityManager.clear();
entityManager.getTransaction().commit(); 

System.out.println( "Czy encja zarzadzana: " + entityManager.contains(h) );

Log aplikacji pokaże, że nasza encja (dzięki wywołaniu metody clear) nie jest już zarządzana w ramach persistence context. Jakiekolwiek zmiany teraz nie zostaną na niej dokonane – nie zostaną odzwierciedlone w bazie automatycznie, ale spróbujmy zastosować wspomnianą metodę merge.


// CIACH - kod z poprzedniego przykładu

entityManager.getTransaction().begin();
h.setName("Wmerdzowany Chlebikowy Mag");
entityManager.merge(h);
entityManager.getTransaction().commit(); 

entityManager.getTransaction().begin();
// Dodatkowe wyczyszczenie kontekstu - dzieki temu mamy 100% pewnosci, ze zmieniona encja zostanie pobrana z bazy danych
entityManager.clear();
entityManager.getTransaction().commit(); 

entityManager.getTransaction().begin();
Hero hh = entityManager.find(Hero.class, 1L);
System.out.println( "Imie maga: " + hh.getName() );
entityManager.getTransaction().commit();

U mnie log powiedział krótko:

Imie maga: Wmerdzowany Chlebikowy Mag

Proces zapisu danych w bazie danych

Wspomniałem wcześniej o próbie zapisywania przez JPA informacji w bazie danych w momencie zakończenia transakcji. Istnieje jednakże możliwość programistycznego wymuszenia zapisu danych w bazie danych – dokonujemy tego za pomocą metody flush. Spowoduje ona próbę zapisania zmian odzwierciedlanych przez persistence context w bazie. Brzmi całkiem prosto, ale niestety z technicznego punktu widzenia takie nie jest – zwłaszcza jeśli mamy na myśli jeszcze nie istniejące w bazie danych encje. Poniżej krótkie przedstawienie zachowania zapisywanej encji w zależności od stanu encji oraz ustawień kaskadowych. Oto przykładowy fragment mapowań z encji bohatera:

@OneToOne(cascade={CascadeType.PERSIST})
@JoinColumn(name="dragon_id")
private Dragon dragon;        

@OneToMany
@JoinTable(name="HERO_WEAPON",
        joinColumns=@JoinColumn(name="HERO_ID"),
        inverseJoinColumns=@JoinColumn(name="WEAPON_ID"))
private List&amp;amp;lt;Weapon&amp;amp;gt; weapons;

Zaś oto kod uruchomieniowy aplikacji:


 // Tworzymy encje broni
 entityManager.getTransaction().begin();
 Weapon w1 = new Weapon();
 w1.setName("Super-Duper-Miecz");
 entityManager.persist(w1);
 entityManager.getTransaction().commit();

 // Czyścimy kontekst - dzięki temu mamy pewność, że encja broni jest w stanie detached
 entityManager.getTransaction().begin();
 entityManager.clear();
 entityManager.getTransaction().commit();

 // Tworzymy bohatera jak do tej pory
 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);

 entityManager.persist( h );
 entityManager.getTransaction().commit();

W jakiej sytuacji jesteśmy? Posiadamy zapisaną encję bohatera. Do tego istnieje encja broni, która jest w stanie detached. Załóżmy, że chcemy dodać teraz smoka.

 entityManager.getTransaction().begin();
 Dragon d = new Dragon();
 d.setName("Smok Wawelski");
 d.setRider(h);
 entityManager.persist(d);
  // Nie konczymy transakcji!!

Nasz smok został stworzony i jest zarządzany przez persistence context. Jednakże bohater nie wie nic o swoim smoku, nie ma też broni. Spróbujmy połączyć to zatem wszystko razem.

// Kontynujemy rozpoczętą transakcję

h.setDragon(d);

List&amp;amp;lt;Weapon&amp;amp;gt; weapons = new ArrayList&amp;amp;lt;Weapon&amp;amp;gt;();
weapons.add(w1);
h.setWeapons(weapons);

entityManager.persist(h);
entityManager.getTransaction().commit();

Przeanalizujmy powyższy kod. Encja bohatera jest w pełni zarządzana. Wyczesany miecz jest w stanie detached. Smok jest zarządzany, ale jest nową encją, która nie została jeszcze zapisana w bazie danych. Co zrobi JPA w powyższej sytuacji?

Przede wszystkim sprawdzi relacje obiektu Hero. Wpierw zajmiemy się smokiem – zasadniczo smok jest zarządzany, ale jest zupełnie nowym obiektem. Tym samym nie posiada jeszcze klucza głównego – w normalnych okolicznościach próby zapisania takiej relacji skończyłoby się wyjątkiem TransientPropertyValueException (w przypadku użycia Hibernate). Jednakże my oznaczyliśmy relację ze smokiem odpowiednimi atrybutami – CascadeType.PERSIST. Dzięki temu nawet niezapisany w bazie danych obiekt zostanie dzięki kaskadowości poprawnie utworzony oraz powiązany z owning side.

Drugim istotnym elementem jest lista broni jakie posiada nasz heros. Specjalnie wyczyściliśmy kontekst, aby mieć pewność, że encja broni będzie od niego odłączona. W deklaracji relacji w klasie Hero nie posiadamy jednakże jakiejkolwiek informacji o operacjach kaskadowych. Jak to zatem możliwe, że odłączona od kontekstu encja może być poprawnie przypisana do naszego bohatera? Jest to wyjątek w działaniu JPA – w przypadku relacji one-to-one lub many-to-one (z punktu widzenia encji broni – czyli posiada zaszyty klucz obcy w sobie) – taka operacja jest jak najbardziej możliwa.

Warto poświęcić chwilę na przetrawienie powyższego by zrozumieć, że zarządzanie encjami oraz zapisywanie relacji nie jest tak proste jak mogłoby się wydawać.

Entity Manager i okolice – część 1

Do tej pory omawiając JPA dość pobieżnie wspomniałem o Entity Managerze. Jest to jednakże jeden z podstawowych elementów całego systemu persystencji i dlatego w dzisiejszym wpisie przyjrzymy się mu z większą uwagą.

Najczęstszym problemem związanym z używaniem JPA jest zrozumienie co tak naprawdę dzieje się z encją. Tudzież podstawowe pytanie brzmi – czy zmiany, które zostały wprowadzone zostały już odzwierciedlone w bazie danych? Pamiętam swoje pierwsze przygody z Hibernate w pracy zawodowej, kiedy dostawałem dziwne komunikaty, że entity is detached from context i co z tym trzeba robić. By zrozumieć cykl życia encji w ramach naszego kodu wpierw trzeba bardziej niż do tej pory przedstawić koncepcję EntityManagera, których to specyfikacja JPA dostarcza 3 typy:

  • container-managed – dobroci tego podejścia są nam dane tylko w przypadku używania serwera aplikacyjnego. By w klasie użyć EntityManagera zarządzanego przez kontener wystarczy, że opatrzymy własność naszej klasy adnotacją @PersistenceContext i w ten sposób mamy gotowego do działania EntityManagera. Nie musimy nic więcej robić – odpowiednio skonfigurowany plik persistence.xml zadziała cuda. To jednak nie koniec – EntityManager zarządzany przez kontener jest dostarczany w dwóch różnych wersjach.
    • transaction-scoped – najczęściej używany. Zwrócony przez kontener obiekt EntityManagera przed wykonaniem jakichkolwiek działań sprawdza, czy znajduje się on w obrębie aktywnej transakcji JTA. Jeżeli tak to (przy pierwszej operacji na z użyciem tego obiektu) jest tworzony PersistenceContext i jest dalej używany do czasu kiedy zakończy się transakcja.
    • extended – jest to typ, który jest dostępny tylko w przypadku statefull beans. Dzięki temu stan przechowywany przez beana jest utrzymywany przez szereg requestów za pomocą jednej konkretnej instancji persistence context (nie jest on niszczony przy każdym requeście, dopiero podczas usuwania beana).

    Obydwa powyższe typy możemy wstrzyknąć do naszych klas z pomocą wspomnianej adnotacji @PersistenceContext. W jaki sposób jednakże je odróżnić? Otóż jednym z atrybutów tej adnotacji jest type, który przyjmuje wartości typu wyliczeniowego PersistenceContextType. Jeżeli atrybut ten nie jest podany wówczas domyślnie używana jest wartość transaction-scoped. Na koniec tego krótkiego omówienia zachęcam do przeczytania pytania na StackOverFlow.

  • application managed – to typ EntityManagera, z którego korzystam tworząc przykłady. Przykładowy kod został przedstawiony w jednym z pierwszych wpisów. W tym przypadku to my sami tworzymy potrzebne obiekty, startujemy i zamykamy transakcję oraz zamykamy EntityManagera. Jest to jedyny sposób używania JPA poza serwerem aplikacyjnym. Oczywiście da się cały bloatware code uproscić choćby za pomocą Springa.

Transakcje

Transakcje są jednym z najważniejszych aspektów JPA. Do podziału przedstawionego powyżej dochodzą też kwestie związane z transakcyjnością, a dokładniej, z typem transakcji. W przypadku używania EntityManagerów zarządzanych przez kontener, domyślnie mamy do czynienia z transakcjami JTA. W przypadku kiedy używamy application-managed możemy wybrać sobie używany typ transakcji, choć domyślnie (w przypadku np. Javy SE) nie mamy dostępu do JTA zatem provider musi obsługiwać tylko natywne transakcje JDBC.

W tym miejscu dobrze jest zapoznać się z kilkoma pojęciami:

  • transaction synchronization – proces dzięki któremu po zakończeniu transakcji zmiany odwzorowane w persistence context zostaną poprawnie zapisane w bazie.
  • transaction association – to proces, w którym persistence context jest łączony z aktywną transakcją.
  • transaction propagation – jest to przekazywanie persistence context między kolejnymi transakcjami (w przypadku wersji extended)

Jeżeli nie widzisz różnicy pomiędzy synchronizacją, a asocjacją to wystarczy napisać, że te dwa procesy różnią się liczebnością. Można bowiem z jedną transakcją zsychronizować dowolną ilość persistence context, ale tylko jeden z nich może być obecnie aktywny (association).

Powinienem teraz napisać porównanie kodu dla container managed oraz application managed, ale przeglądając sieć znalazłem fenomenalny materiał Piotra Nowickiego, w którym poczytacie dokładnie o tym, co miałem opisywać.

Wspomnę w tym miejscu o cofaniu (rollback) transakcji. Jest to bowiem dość istotne, dotyczy też wszystkich operacji, które możemy wykonywać z pomocą EntityManagera. Otóż w przypadku kiedy operacja w bazie danych nie powiedzie się dostaniemy wyjątek, ale co najważniejsze nasze encje zostaną ‘odłączone’ (detached) z obecnego kontekstu! Jest to o tyle istotne, gdyż wiele osób łapie wyjątek po czym próbuje zapisywać encje jeszcze raz dziwiąc się, że dostaje dziwne błędy. Z całą pewnością takie coś nie zadziała. Natomiast można spróbować stworzyć nowy kontekst, podpiąć do niego te encje (napiszę o tym niedługo) po czym znów próbować zapisu. W takiej jednak sytuacji warto brać pod uwagę dwie rzeczy:

  • klucz główny – encje po cofnięciu transakcji są w takim stanie jakie ‘zastała’ je transakcja w bazie danych. Czyli o ile zmiany zostały wycofane z bazy danych, o tyle wartości w naszych encjach zostają ‘po staremu’. Jest to istotne w sytuacji, w której do generowania klucza głównego używamy sekwencji. Wówczas w bazie danych może ona zostać cofnięta o określoną wartość, ale nasza encja dalej posiada tę wartość jako klucz główny. Może okazać się zatem, że będziemy po chwili próbować zapisywać w bazie danych obiekt z ID, który już został użyty. By uniknąć tego typu sytuacji należy wyczyścić klucz główny przed ponownym zapisem.
  • pole wersjonujące – może mieć niepoprawną wartość.

Do tej pory używaliśmy API EntityManagera dość pobieżnie opisując metody służące zmianie encji. W tym miejscu wypada napisać kilka słów więcej, zwłaszcza mając na uwadze persistence context i transakcyjność.

Tworzenie encji

Metoda ta przyjmuje jako argument nowy obiekt, który dopiero chcemy dodać do kontekstu. Dlaczego nowy? Ano bo w przypadku próby zapisu istniejącej encji zachowanie aplikacji zależy od providera – na 99% poleci wyjątek, ale kiedy – to już pytanie. Może już w momencie dodawania obiektu do kontekstu, może dopiero przy końcu transakcji. W przypadku transaction scoped entity managera metoda ta musi być wykonywana przy aktywnej transakcji. Pozostałe dwa typy EntityManagera zaakceptują wywołanie metody, dodadzą do kontekstu, ale z jakimikolwiek działaniami poczekają do czasu kiedy pojawi się aktywna transakcja.

Wyszukiwanie encji

Wyszukiwanie encji jest chyba najczęściej wykonywaną operacją w przypadku baz danych. EntityManager dostarcza metodę find, którą mieliśmy już okazję używać. Wywołanie metody (oczywiście poprawne i zakończone sukcesem) zwraca zarządzaną encję. Jedynym wyjątkiem jest znów przypadek transaction scoped entity managera, gdzie zwrócona encja jest w stanie detached. Co ciekawe istnieje bardzo fajna metoda, którą poznałem dopiero teraz – getReference. Kod z książki jest moim zdaniem bardzo wiele mówiący.

Department dept = em.getReference(Department.class, 30);
Employee emp = new Employee();
emp.setId(53);
emp.setName("Peter");
emp.setDepartment(dept);
dept.getEmployees().add(emp);
em.persist(emp); 

Jak widać nie jest potrzebne wykonywanie dwa razy metody find – wystarczy metoda getReference by po prostu otrzymać odpowiedni obiekt tylko z kluczem głównym, który jednakże przyda się świetnie do stworzenia odpowiedniej relacji. Należy jednakże pamiętać, iż dobrze by było, aby encja taka istniała. Omawiana metoda zwraca proxy tej encji, zatem istnieje szansa, że przy próbie zapisu do bazy danych po zakończeniu transakcji zostanie zwrócony wyjątek, kiedy proxowana encja nie zostanie znaleziona. Warto o tym pamiętać.

Usuwanie encji

Wydawałoby się, że nie ma nic prostszego niż usuwanie encji. Podajemy ją jako argument metody remove i pufff, encji nie ma (oczywiście po commicie transakcji). W przypadku transaction-scoped entity managera kiedy spróbujemy usunąć niezarządzaną encję dostaniemy TransactionRequiredException. Pozostałe typy entity managera mogą usuwać niezarządzane encje, ale zmiana zostanie odzwierciedlona w bazie danych dopiero po commicie transakcji.

Problemem jest również usuwanie encji wraz z zdefiniowanymi w niej relacjami. Jest to temat powiązany z kaskadowością operacji, o czym napiszę w dalszej części. Na razie spróbujmy zabić smoka naszego bohatera – ustaliliśmy, że kiedy smok ubit wówczas nasz heros może sobie co najwyżej konia znaleźć. Zatem spróbujmy.

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
entityManager.remove(dd);

Uruchamiamy kod i co? Ano to:

Referential integrity constraint violation: “FK7ECFBA2844FC6C27: PUBLIC.HEROES FOREIGN KEY(DRAGON_ID) REFERENCES PUBLIC.DRAGON(ID) (1)”; SQL statement:
delete from Dragon where id=? [23503-145]

Dlaczego tak? Ano w naszym kodzie definiującym zależność między smokiem i bohaterem wskazaliśmy bohatera jako stronę rodzicielską. Zatem w tabeli z bohaterem znajduje się kolumna, która trzyma informację o identyfikatorze smoka. Kiedy spróbujemy usunąć smoka następuje naruszenie więzów integralności i dostajemy powyższy wyjątek. By tego uniknąć musimy wpierw ‘wyzerować’ relację, a dopiero potem ubić gadzinę. Powyższy kod powinien zatem wyglądać następująco:

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
hh.setDragon(null);
entityManager.remove(dd);

I dopiero teraz poprawnie da się usunąć naszego smoka. Zarządzanie tego typu zależnościami (przy usuwaniu) należy do aplikacji i jest zadaniem programisty.

Operacje kaskadowe

Domyślnie JPA nie robi niczego ponad to, czego jasno sobie zażyczyliśmy. Jeśli zatem jak w powyższym przykładzie chcemy usunąć encję czy też zależności między nimi musimy zrobić to jasno deklarując swoje zamiary. Jednakże ORM powstał by ułatwiać pracę programisty, a nie dodawać jeszcze więcej roboty. Stąd też istnieje możliwość poinstruowania JPA by pewne rzeczy robił za nas automatycznie.

Wracając do przykładu ze smokiem i herosem – załóżmy przez chwilkę, że smok powstaje wraz z naszym bohaterem (co w sumie zresztą do tej pory uskutecznialiśmy). Jednakże do tej pory tworzyliśmy oddzielnie instancję smoka, a następnie dodawaliśmy ją do naszego bohatera. Obydwie encje były zapisywane oddzielnie. Teraz zaś by osiągnąć ten sam efekt skorzystamy z dobrodziejstwa typu wyliczeniowego CascadeType, który podany jako atrybut dla adnotacji relacyjnych pozwala sterować zachowaniem operacji kaskadowych. Domyślną wartością tego atrybutu ( cascade ) jest brak jakichkolwiek działań. Jeśli jednak chcemy zmienić to zachowanie mamy do wyboru kilka opcji:

  • ALL – wszystkie operacje danej relacji będą kaskadowane
  • DETACH – tylko odłączenie encji od kontekstu persystencji
  • MERGE – aktualizacja encji
  • PERSIST – zapisanie encji
  • REFRESH – odświeżenie stanu encji (pobranie stanu z bazy danych i zastąpienie obecnego)
  • REMOVE – usuwanie encji

Dodajmy zatem odpowiednie wartości do naszego bohatera. Kod relacji wygląda teraz tak:

// Zwróc uwagę na nietypową składnię wartości - atrybut cascade musi mieć dostarczoną tablicę!!!
@OneToOne(cascade={CascadeType.PERSIST})  
@JoinColumn(name="dragon_id")
private Dragon dragon;

W kodzie wykonywalnym możemy dzięki temu zastosować taką konstrukcję:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");

Dragon d = new Dragon();
d.setName("Smok Wawelski");
				
h.setDragon(d);            
entityManager.persist( h );

I tym samym stworzyliśmy smoka jednocześnie tworząc bohatera. Nie ma potrzeby dla dwukrotnego wywoływania persist na każdej encji z osobna. Należy jednak zauważyć, że odwrotna zależność (zapisujemy smoka, a nie bohatera) nie jest możliwa. Operacje kaskadowe są wybitnie jednostronne (lub bezstronne – unidirectional) – spodziewane zachowanie musimy wskazać w każdym miejscu relacji. W naszym przykładzie kod encji smoka nie został zmieniony, ergo, przy zapisie smoka na pewno nie nastąpi kaskada operacji zapisu i tym samym przy próbie zapisu odwrotnego dostaniemy wyjątek:

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");

Dragon d = new Dragon();
d.setName("Smok Wawelski");
d.setRider(h);

entityManager.persist( d );

Wyjątek:

java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance – save the transient instance before flushing: com.wordpress.chlebik.jpa.domain.Dragon.rider -> com.wordpress.chlebik.jpa.domain.Hero

O ile tworzenie obiektów poprzez operacje kaskadowe wydaje się być dość wygodnym rozwiązaniem, o tyle usuwanie encji będących w relacji z innymi brzmi w ogóle bardzo miło. Niestety w przypadku usuwania rekordów nie jest tak różowo. Z całą pewnością należy uważać co usuwamy i dlaczego, a także należy zdać sobie sprawę z sytuacji, o której pisałem w poprzednim paragrafie – nie istnieje możliwość stworzenia kaskadowej relacji między encjami, które pozwoliłoby usunąć dziecko z relacji z rodzicem bez usuwania rodzica. Czyli usunięcie smoka nie spowoduje tego, że zniknie zapisany klucz obcy w tabeli bohatera. Musimy tę operację przeprowadzić ręcznie. Gdybyśmy bowiem zapisali taki kod:

// Kod klasy Hero
@OneToOne(cascade={CascadeType.REMOVE}) 
@JoinColumn(name="dragon_id")
private Dragon dragon;

// Kod klasy Dragon
@OneToOne(mappedBy="dragon",cascade={CascadeType.REMOVE})
private Hero rider;

Zaś następnie w kodzie uruchomieniowym zastosowali takie coś:

Hero hh = entityManager.find( Hero.class, 1l );
Dragon dd = hh.getDragon();
entityManager.remove(dd);

Kod nie zrobiłby tego, czego od niego oczekiwaliśmy. W logu zapytań SQL widać to bardzo wyraźnie:

Hibernate: delete from HEROES where id=?
Hibernate: delete from Dragon where id=?

Czyli usuwając smoka usunęliśmy też jeźdźca. No nie do końca o to chodziło. Kiedy zaś usuniemy atrybut cascade z klasy Dragon, wówczas przy wykonaniu powyższego kodu (przynajmniej w najnowszej wersji Hibernate) nie wykona się nic! Dziwnym trafem wpisanie atrybutu cascade po stronie bohatera sprawia, że próba usunięcia smoka nie powoduje wygenerowania choćby próby skasowania rekordu z bazy danych (co w sumie może być dobre, albo i nie). Jeśli jednakże spróbujemy usunąć naszego bohatera w całości.

Hero hh = entityManager.find( Hero.class, 1l );
entityManager.remove(hh);

W logu SQL zobaczymy ciekawe rzeczy:

Hibernate: delete from Hero_nicknames where Hero_id=?
Hibernate: delete from HEROES where id=?
Hibernate: delete from Dragon where id=?

Jak widać JPA dokładnie przyjrzało się strukturze encji bohatera i usunęło wszystkie zależności (przydomki naszego bohatera są integralną częścią encji, niezależnie od faktu, że są kolekcją) w ten sposób, aby nie naruszać więzów integralności i tym samym zapewnić poprawność działania aplikacji. Jak widać usuwanie encji za pomocą operacji kaskadowych nie jest aż takie oczywiste jak mogłoby się wydawać.

Mapowanie kolekcji

W poprzednim wpisie skupiłem się na mapowaniu relacji pomiędzy encjami. Jednakże praktyka programistyczna jasno pokazuje, iż dość często w użyciu są kolekcje typów prostych (lub ich wrapperów). W przypadku JPA mogą też (o czym jeszcze nie pisałem) występować kolekcje obiektów zagnieżdżonych. I dlaczego tylko mowa o kolekcjach w taki prosty sposób? Przecież mamy i listy i mapy no i nawet zbiory. Dzisiejszy wpis będzie dotyczył możliwości JPA jeśli chodzi o wszystkie powyższe zagadnienia.

Typy proste i embedded

Relacje między encjami w przypadku JPA wymagają użycia adnotacji pokazującej ‘krotność’ relacji (@OneToMany, @ManyToMany, itp). W przypadku typów prostych i embedded nie ma takiej potrzeby. By oznaczyć, że konkretna encja zawiera w sobie kolekcję tego typu obiektów wystarczy zastosować adnotację @ElementCollection. Załóżmy, że chcemy obdarować naszego bohatera listą jego przydomków (Chlebik the Butcher, Ripper Chlebik, itp) – wyglądałoby to tak:

@ElementCollection
private Collection nicknames;

// Getterki i setterki pominięte

Jeśli uruchomimy kod z poprzednich rozdziałów i podejrzymy strukturę bazy danych, zobaczymy nową tabelę o nazwie HERO_NICKNAMES. Zawiera ona oczywyście klucz obcy do tabeli HEROES oraz wartość tekstową. Adnotacja @ElementCollection nie potrzebowała dodatkowych atrybutów, gdyż przy używaniu generyków automatycznie wie jakiego typu obiekt znajduje się w kolekcji. Domyślnym zaś typem pobierania relacji jest EAGER (można to zmienić w formie podpowiedzi dla providera poprzez atrybut fetch). Określenie docelowej tabeli przechowującej informację o kolekcji jest możliwe za pomocą adnotacji @CollectionTable, w której możemy podać dość standardowe atrybuty jak nazwę tabeli, schemat, kolumny łączące czy dodatkowe ograniczenia (jak zawsze zachęcam do zapoznania się z dokumentacją). Istnieje też możliwość zmiany nazwy kolumny przechowującej informacje o przydomkach (adnotacja @Column), a także w przypadku typów embedded zmienić dla tej konkretnej relacji nazwy kolumn w bazie danych adnotacją @AttributeOverride).

Interfejsy szczegółowe – Set

Powyższy kawałek kodu operował dość uniwersalnym interfejsem Collection. Każdy programista Javy wie jednak, że na co dzień ma do dyspozycji o wiele większy wachlarz możliwości. Zaczniemy od tego najprostszego – interfejsu Set. W przypadku kolekcji nie będących encjami zachowuje się on bardzo podobnie do interfejsu Collection z jednym zasadniczym wyjątkiem – nie dopuszcza do użycia duplikatów w kolekcji. Zamieńmy podany wyżej przykład kodu:

@ElementCollection
private Set nicknames;

// Getterki i setterki pominięte

zaś przy deklaracji naszego bohatera użyjmy:

Set nickNames = new HashSet();
nickNames.add("Ripper");
nickNames.add("Ripper");
nickNames.add("Butcher");
h.setNicknames(nickNames);

W bazie danych po zapisaniu obiektów zostaną zapisane tylko i wyłącznie 2 wartości – ‘Ripper’ oraz ‘Butcher’.

Interfejsy szczegółowe – List

Użycie interfejsu List jest wymagane wówczas, kiedy chcemy by elementy naszej kolekcji były w jakiś sposób poukładane (posortowane). Możemy osiągnąć to na dwa sposoby:

  • przechowując informację o tym fakcie w bazie danych – tworzymy oddzielną kolumnę w tabeli, której jedynym celem jest przechowywanie informacji o kolejności danej wartości na liście. Można to osiągnąć za pomocą adnotacji @OrderColumn. Domyślnie tworzy ona kolumnę o nazwie składającej się z nazwy naszej własności w encji i końcówki ‘_ORDER’. Jeżeli nasz kod encji zamienimy na:
    @ElementCollection
    @OrderColumn
    private List nicknames;
    

    To przy dodawaniu przydomków w ten sposób:

    // Zmiana dodawania przydomków do bohatera
    List nickNames = new ArrayList();
    nickNames.add("Ripper");
    nickNames.add("Butcher");
    nickNames.add("Killer");
    h.setNicknames(nickNames);
    

    i zapisaniu danych w bazie danych zobaczymy, że powstała dodatkowa kolumna o nazwie NICKNAMES_ORDER, w której (zaczynając od wartości 0!) mamy zapisane kolejne cyfry pokazujące kolejność przydomków. Teraz wystarczyłoby poprzestawiać kolejność obiektów w encji, np:

    entityManager.getTransaction().begin();
    
    Hero hh = entityManager.find(Hero.class, 1L);
    List nickNames = hh.getNicknames();
    nickNames.remove("Ripper");
    nickNames.add("Puncher");
    
    entityManager.persist(hh);
    
    entityManager.getTransaction().commit();
    

    by zobaczyć w bazie danych, że nasze dane zostały uaktualnione (co zresztą również widać w logu Hibernate – zostały wygenerowane 3 zapytania UPDATE). Powyższa metoda jest z całą pewnością dobra w przypadku encji i typów wbudowanych. Należy mieć jednak na uwadze jedną dość istotną sprawę – wydajność. Jak widać jakakolwiek zmiana kolejności wymaga aktualizacji danych WSZYSTKICH WARTOŚCI. W przypadku częstych zmian i pod dużym obciążeniem możemy całkiem nieźle rozgrzać bazę danych. Dobrze jest mieć to na uwadze.

  • używając jednego z atrybutów przechowywanego obiektu (dotyczy encji i typów wbudowanych) – wykorzystując adnotację @OrderBy. Istotny jest fakt, że możemy adnotacji tej używać również w przypadku zwykłych relacji (czyli z użyciem adnotacji @OneToMany i podobnych, wówczas domyślnie zakładamy sortowanie po kluczu głównym). Adnotacja ta nie posiada atrybutów, a jedynie wartość – jest to łańcuch tekstowy obejmujący listę używanych do sortowania własności z opcjonalną informacją o kolejności (ASC, DESC – domyślnie ASC). Nasze przydomki póki co są zwykłymi łańcuchami tekstowymi, ale co gdybyśmy przerobili je na obiekt wbudowany?
    @Embeddable
    public class Nickname {
    
    private String nick;
    
    // Uciete getterki/setterki oraz ew. dodatkowe
    // wlasnosci, ktore pewnie sprawilyby, ze ten
    // obiekt mialby wiecej sensu 😉
    
    }
    

    Przy tworzeniu naszego bohatera użylibyśmy takiego kodu:

    List nickNames = new ArrayList();
    Nickname n1 = new Nickname(); n1.setNick("Ripper");
    Nickname n2 = new Nickname(); n2.setNick("Butcher");
    Nickname n3 = new Nickname(); n3.setNick("Killer");
    nickNames.add(n1);
    nickNames.add(n2);
    nickNames.add(n3);
    h.setNicknames(nickNames);
    

    Jeśli zatem chcielibyśmy posortować nasze przydomki alfabetycznie przy pobieraniu encji z bohaterem musielibyśmy zrobić coś takiego:

    // Zmiana encji
    @ElementCollection
    @OrderBy("nick ASC")
    private List nicknames;
    
    // Kod wyciagajacy encje
    Hero hh = entityManager.find(Hero.class, 1L);
    List nickNames = hh.getNicknames();
    
    for( Nickname nn : nickNames ) {
        System.out.println("Nick: " + nn.getNick() );
    }
    

    Efekt działania kodu to:

    Nick: Butcher
    Nick: Killer
    Nick: Ripper

    Czyli mamy pięknie posortowane alfabetycznie obiekty bez konieczności dodatkowej pętli przy pobieraniu danych czy tym podobnych udziwnień.

Interfejsy szczegółowe – Mapy

Przyznam szczerze, że z mapami jest problem. Albowiem do tej pory w omawianym temacie pojawiały się w użyciu trzy typy – proste, encje i embedded. Teraz dodajmy do tego fakt, że mapy posiadają nie tylko wartość, ale również klucz, który może być przecież również typem prostym, encją lub embedem. Łącznie dziewięć kombinacji. Od razu stwierdzę, że przepisywanie książki tutaj niespecjalnie ma sens dlatego ograniczę się do kilku zdań wstępu, a potem po prostu skopiuję ładną tabelkę i zachęcę do odwiedzenia dokumentacji 😉

W przypadku map kwestią do rozwiązania jest miejsce składowania kluczy, pod którymi będą trzymane wartości kolekcji. Zasadniczo klucz niczym się zasadniczo nie różni (technicznie) od informacji o kolejności danej wartości na liście. Zatem rozsadnym wydaje się być przechowywanie klucza mapy w tej samej tabeli, w której znajdują się nasze wartości. W przypadku tabel łączących (jak np. w relacji many-to-many) ta tabela jest najlepszym miejscem by składować tę informację. Pod kątem struktury połączenia najlepiej będzie gdy oddam głos autorom książki:

It is always the type of the value object in the Map that determines what kind of mapping must be used. If the values are entities, the Map must be mapped as a one-to-many or many-to-many relationship, whereas if the values of the Map are either embeddable or basic types, the Map is mapped as an element collection.

Zakładam, że powyższe jest dość jasne. Do tworzenia połączeń i mapowań wykorzystać można kilkanaście adnotacji:

  • @MapKeyColumn – wskazuje na kolumnę, w której będzie przechowywany klucz dla konkretnych wartości mapy w przypadku gdy kluczem jest wartość prosta!
  • @MapKeyEnumerated – sytuacja bardzo podobna jak w powyższym punkcie, z tym tylko, że rolę klucza odgrywa typ wyliczeniowy (ENUM). Ze strony JPA typ wyliczeniowy jest uznawany za typ prosty (ponieważ ‘pod spodem’ jest reprezentowany przez łancuch tekstowy lub liczbę porządkową)
  • @MapKeyTemporal – sytuacja dotyczy klucza będącego typu java.util.Date lub java.util.Calendar – dokładną klasę określa wartość adnotacji.
  • @MapKey – adnotacja używana wówczas, gdy kluczem mapy będzie jedna z własności prostych obiektów, które będą wartościami.
  • @MapKeyJoinColumn – używana w sytuacji kiedy kluczem jest encja. W tabelach bazy danych (konkretnie w jednej z kolumn) jest zapisywana o kluczu głównym encji (nie przechowujemy jej samej).
  • @MapKeyClass – adnotacja używana w sytuacji kiedy nasza mapa nie korzysta z generyków. Wówczas konieczne jest użycie tej adnotacji wraz z podaniem (jako atrybut) klasy typu, który będzie używany jako klucz.

To powiedziawszy poniżej zamieszczam ładną tabelkę, którą przekopiowałem z książki 😉

Map Mapping Key annotation Value annotation
Map<Basic, Basic> @ElementCollection @MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
@Column
Map<Basic, Embeddable> @ElementCollection @MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map<Basic,  Entity> @OneToMany, @ManyToMany @MapKey,
@MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
Mapped by entity
Map @ElementCollection Mapped by embeddable,
@AttributeOverride
@Column
Map @ElementCollection Mapped by embeddable,
@AttributeOverride
Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map @OneToMany, @ManyToMany Mapped by embeddable Mapped by entity
Map<Entity, Basic> @ElementCollection @MapKeyJoinColumn @Column
Map<Entity, Embeddable> @ElementCollection @MapKeyJoinColumn Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map<Entity, Entity> @OneToMany, @ManyToMany @MapKeyJoinColumn Mapped by entity

ORM – Relacje

Bardzo wiele osób utożsamia JPA z używaniem ORM. Jest to zasadniczo wcale nie aż tak głupie myślenie – przy prostych projektach bez większych problemów poradzić można sobie tworząc kilka klas domenowych oraz odwołując się do nich prostymi metodami z EntityManagera. By jednakże w pełni to osiągnąć trzeba poruszyć bardzo ważny aspekt ORM – relacje.

Współczesne bazy danych nie od biedy nazywane są relacyjnymi – dane przechowywane są w oddzielnych tabelach, kluczowe zaś są relacje jakie zachodzą między nimi. Zrozumienie podstaw ORM jest dość łatwe – opanowanie arkanów mapowania relacji oraz tego w jaki sposób będą się one zachować jest cokolwiek trudniejsze. Dlatego też temu zagadnieniu poświęcę dzisiejszy wpis. By jednakże w spokoju pracować nad relacjami potrzebujemy kolejnych obiektów domenowych, aby móc się do nich odwoływać. Przykładową encją niech będzie smok (a co, każdy bohater może mieć smoka):

@Entity
public class Dragon {

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

    private String name;

    public Dragon() { }

    // Gettery i settery pominiete
}

W podobny sposób trzeba zadeklarować kilka obiektów, które wyglądają dokładnie tak samo, tylko mają inną nazwę. Dodamy zatem jeszcze Weapon (sztuka broni) oraz Deity (bóstwo). Dzięki temu tłumaczenie relacji będzie delikatnie mówiąc – łatwiejsze.

Relacje one-to-one

Zasadniczo są one najłatwiejsze do zrozumienia zatem od nich zaczniemy. Relacja tego typu zakłada sytuację, w której dokładnie jednej encji odpowiada dokładnie jedna inna encja. Przykładem z życia jest choćby człowiek i jego numer PESEL – każdy człowiek ma jeden numer pesel, każdy pesel reprezentuje jednego człowieka – tertium non datur. Na przykładzie naszej gry będzie to relacja pomiędzy naszym bohaterem i smokiem (pomysł pochodzi z serii Eragon). Każdy bohater ma dokładnie jednego smoka, nikt inny tego smoka dosiąść nie może. Co więcej – jak smoka ubiją (lub się nażre czegoś, potem wypije Wisłę wody i pęknie) nasz bohater jest skazany na konie i własne nogi. Perfekcyjna relacja one-to-one. Naszą bohaterską encję musimy rozszerzyć o obiekt ze smokiem.

@OneToOne
@JoinColumn(name="dragon_id")
private Dragon dragon;

Tadam, mamy smoka 😉 Adnotacje omówię za chwilkę – naszego smoka dodajemy do naszego bohatera w ten sposób.

Dragon d = new Dragon();
d.setName("Smok Wawelski");
entityManager.persist( d );
hero.setDragon(d);

Jak widać dodanie relacji do encji jest banalnie proste. Wyjaśnienia wymagają z całą pewnością użyte adnotacje. @OneToOne należy do grupy adnotacji, które pojawiają się zawsze przy mapowaniu relacji (inne nazywają się równie obrazowo). Zalecam zapoznanie się z dokumentacją dotyczącą atrybutów tej adnotacji – na przykładzie powyżej pozostawiłem wszystkie wartości domyślne i zasadniczo działa to dobrze. Akurat twórcy JPA pomyśleli sensownie w tym przypadku.

Drugą istotną adnotacją jest @JoinColumn. Zgodnie z nazwą określa ona jaka kolumna (no i z jakimi atrybutami) będzie odwzorowywać relację. W naszym przypadku nasz bohater jest stroną posiadającą (owning side) relację. W związku z tym będziemy trzymać w wygenerowanej tabeli z bohaterami referencję do tabeli ze smokami (klucz obcy). W powyższym przykładzie wskazałem nazwę kolumny – domyślnie jest ona tworzona z nazwy własności, podreślnika + kilka zasad – jak zawsze polecam zapoznanie się z dokumentacją by poznać szczegóły.

Relację, którą pokazałem na powyższym przykładzie możemy nazwać jednostronną (unidirectional). Nasz heros wie wszystko o swoim smoku, ale sam smok nie bardzo ma pojęcie o istnieniu naszego bohatera. Czyli zamiast grzecznie dać się ujeździć prędzej zeżre potencjalnego jeźdźca. Zresztą w opisie smoka wyszło nam jasno, że smok ma tylko jednego jeźdźca, zatem dobrze by było, aby coś o nim wiedział. Taką relację (gdzie obie strony wiedzą o sobie) nazywamy bidirectional.

@OneToOne(mappedBy="dragon")
private Hero rider;

Teraz nasz smok również ma wiedzę o drugiej stronie relacji. W przypadku bazy danych nie zmieniło się nic – jednakże nie ma problemu by wykonać następujący kod:

Dragon dragon = entityManager.find(Dragon.class, 1L);
Hero rider = dragon.getRider();
System.out.println("Jezdziec nazywa sie " + rider.getName() );

U mnie jeźdźcem jest ‘Chlebikowy mag’ 😉

Relacje many-to-one i one-to-many

Wspomniałem na samym początku by utworzyć też encję reprezentującą broń (Weapon). Zasadniczo zgodzimy się, że każdy bohater może mieć kilkanaście sztuk broni (jakiś mieczyk, sztylet, może i łuk dla odmiany). Mamy zatem kilkanaście sztuk konkretnego przedmiotu, które przynależą do jednej encji (naszego bohatera). Podręcznikowy zatem przykład relacji many-to-one. W jej przypadku to strona many jest ‘posiadaczem’ relacji, gdyż to w niej będzie zapisany klucz obcy do encji bohatera. Ma to zasadniczo sens – każda sztuka broni trzyma informację o swoim właścicielu. Sam zaś bohater (na poziomie tabeli w bazie danych) nie ma o broni pojęcia. W klasie broni dodajemy zatem taki kod:

@ManyToOne
private Hero owner;
// Getterki i setterki pominięte

Tym samym każda sztuka broni posiada referencję do swojego posiadacza. Jak już wspomniałem w przypadku takiej relacji to strona obdarzona adnotacją @ManyToOne jest stroną posiadającą. Dzięki temu w tabeli Weapon będzie składowana kolumna z kluczem obcym do encji bohaterów. Przy domyślnym zachowaniu adnotacji – zostanie ona wygenerowana z nazwy własności encji oraz nazwy kolumny z kluczem głównym w docelowej encji (czyli u nas będzie to kolumna owner_id w tabeli Weapon). Podobnie jak w przypadku poprzednich relacji możemy sterować tą relacją za pomocą adnotacji @JoinColumn. Moglibyśmy zatem nasz kod zmodyfikować by wyglądał w ten sposób:

@ManyToOne
@JoinColumn(name="hero_id")
private Hero owner;

Co sprawia, że przynajmniej na poziomie bazy danych nasza tabela jest trochę bardziej jednoznaczna i czytelna. W powyższym kodzie powtarzamy jednak sytuację z bohaterem i jego smokiem. Z całą pewnością dobrze by było, aby bohater wiedział jaką broń ma do dyspozycji. Wtedy będzie to niejako odwrócenie relacji many-to-one, czyli będziemy mieć do czynienia z relacją typu one-to-many. Zmodyfikujemy zatem klasę Hero.

@OneToMany(mappedBy="owner")
private List weapons;

Jak już wspomniałem bohater jest w tej stronie stroną podrzędną w relacji (inverse side) – jedynym zatem elementem poza adnotacją @OneToMany jest wskazanie na własność, która jest posiadającą relację w klasie Weapon (czyli na owner). Istnieje jednakże możliwość, aby nie specyfikować atrybutu mappedBy. W tym przypadku zajdą zmiany na poziomie bazy danych (przy użyciu tego atrybutu tak naprawdę wszystko pozostaje po staremu) – powstanie tabela łącząca encję bohatera z bronią. Kod encji broni w tym przypadku nie posiadałby w ogóle informacji o właścicielu (należy usunąć własność owner), zaś encja bohatera powinna wyglądać tak:

@OneToMany
@JoinTable(name="HERO_WEAPON",
	joinColumns=@JoinColumn(name="HERO_ID"),
	inverseJoinColumns=@JoinColumn(name="WEAPON_ID"))
private List weapons;

W tym momencie powstaje tabela łącząca o nazwie HERO_WEAPON z nazwami kolumn jak podaliśmy w adnotacji @JoinTable. Na poziomie bazy danych encje biorące udział w relacji w ogóle nie wiedzą o swym istnieniu (podobnie jak w przypadku relacji many-to-many nie posiadają kolumn z kluczami obcymi w tabelach).

Relacje many-to-many

Ten typ relacji jest równie łatwy do zrozumienia co jeden do jednego. Relacja wiele-do-wielu zakłada istnienie tabeli pośredniczącej. W tabeli tej występują dwie kolumny – z parami kluczy obcych wskazującymi na encje znajdujące się w innych tabelach. Przykładem takiej relacji są bóstwa (w naszej grze RPG istnieje politeizm). Jedno bóstwo może mieć miliony wyznawców, ale też i każdy bohater może jednocześnie wyznawać kilka bóstw. Zaczniemy zatem od modyfikacji naszego bohatera:

@ManyToMany
List deities;

Jak i również trzeba w klasie bóstwa dorzucić wyznawców:

@ManyToMany
List believers;

I zasadniczo nie ma nic więcej do dodania. Jeśli uruchomimy kod aplikacji (z create-drop w persistence.xml), wówczas pojawi się w bazie danych ciekawa sytuacja – dwie tabele łączące! Dlaczego tak? W przypadku relacji wiele-do-wielu nie można (w sensie logicznym) wskazać właściciela tej relacji. Jednakże programista musi dokonać arbitralnego wyboru tej encji, która zostanie potraktowana jako właściciel relacji, zaś tym samym druga strona relacji staje się podrzędną. Załóżmy, że w powyższym przykładzie za posiadającą uznamy encję bóstwa (tak dla odmiany).

@ManyToMany
@JoinTable(name="DEITY_HERO",
	joinColumns=@JoinColumn(name="DEITY_ID"),
 	inverseJoinColumns=@JoinColumn(name="HERO_ID"))
List believers;

Bohater zaś jest elementem ‘podrzędnym’:

@ManyToMany(mappedBy="believers")
List deities;

Przy powyższym kodzie w bazie danych powstanie tylko jedna tabela łącząca zawierająca klucze obce. Więcej ciekawostek można znaleźć w dokumentacji adnotacji @ManyToMany

Sposoby pobierania danych

W zależności od typu relacji dane pobierane są przez JPA w różny sposób. Sensowniej będzie podeprzeć się cytatem z książki:

The fetch mode can be specified on any of the four relationship mapping types. When not specified on a single-valued relationship, the related object is guaranteed to be loaded eagerly. Collectionvalued relationships default to be lazily loaded, but because lazy loading is only a hint to the provider, they can be loaded eagerly if the provider decides to do so.

Dość często spotykanym podejściem jest specyfikowanie różnego sposobu pobierania (poprzez adnotację @FetchType) – kiedy pobieramy bohatera może nie jest dobrym od razu pobierać całą jego broń, a w późniejszym pewnie terminie całe drzekwo umiejętności, bóstw i tak dalej. Wtedy specyfikujemy relacje jako lazy-loaded. Jednakże kiedy pobieramy informacje o konkretnej sztuce broni – dobrze by było wiedzieć kto jest jej właścicielem. W sumie w końcu jak np. naostrzyć broń bez właściela – wszak ktoś musi za to zapłacić 😉

ORM – podstawy

W kolejnej części przygody z JPA zajmiemy się samym mięskiem – czyli mapowaniem relacyjno-obiektowym. W jaki sposób obiekty są odwzorowywane na bazę danych, jak się łączą (o relacjach będzie w kolejnym wpisie) oraz jak da się je jednoznacznie identyfikować. Temat dość długi i obszerny zatem do roboty.

Dostęp do pól encji oraz ich mapowanie

W poprzednim wpisie dość pobieżnie pokazałem kilka adnotacji używanych w encjach. Wystarczyło to, aby pokazać działający przykład. Teraz skupimy się troszkę bardziej dokładnie nad własnościami encji. JPA może używać własności obiektu na dwa sposoby – za pomocą dostępu do pól (field access) lub dostępu do własności (nie wiem czy dobrze to tłumaczę, w oryginale property access).

  • field access – dostęp providera bezpośrednio do pól encji. Nie są potrzebne gettery/settery, zaś jeśli występują są ignorowane. Pola encji nie powinny mieć dostępu publicznego (choć nie jest to wymóg JPA, ale dobra praktyka).
  • property access – przy uzyciu tego trybu dostępu encja musi spełniać kontrakt JavaBeans – posiadać odpowiednie gettery i settery. Typ pola jest określany na podstawie typu zwracanego przez getter, zaś metody dostępowe muszą rzecz jasna mieć poziom dostępu public lub protected.

Mógłbyś się zapytać czytelniku – co jest domyślnym sposobem dostępu? Ano tutaj sprawa jest dość prosta – wystarczy, że umieścisz adnotację @Id nad polem encji oznaczającym klucz główny i zostanie użyty dostęp field. Jeśli dokonasz tego na metodzie getId() włączy się property access. Innym sposobem jest użycie adnotacji @Access, którą można umieścić na poziomie klasy bądź pól/metod (wraz z jej własnościami – AccessType. Jest to nowość wprowadzona w wersji drugiej JPA. Nie ma zatem problemu by część własności była dostępna poprzez pola, a część poprzez gettery. Jednakże takie mieszanie nie jest dobre – zaciemnia kod. Może być przydatne w przypadku dodawania nowych pól w encjach dziedziczących po rodzicach (możemy nie mieć do tychże rodziców dostępu), bądź też chcąc dokonać jakiejś dodatkowej transformacji wartości.

Na sam koniec omawiania dostępu do własności encji należy wspomnieć o możliwości zablokowania zapisu danych do bazy. Służy do tego adnotacja @Transient. Różni się ona od działania słowa kluczowego transient w Javie, zaś najlepiej wyjaśnia to StackOverflow.

W pierwszym wpisie wspomniałem o adnotacji @Table. Najczęściej pozostawia się ją samodzielną – nie zmieniając atrybutów i zachowując domyślne wartości. Wyjątkiem może być nazwa docelowej tabeli, do której trafi dana encja. Służy do tego atrybut name. Z ciekawych atrybutów (radzę poczytać sobie JavaDoc dla wszystkich adnotacji, to naprawdę pomaga) można wspomnieć uniqueConstraints – dodatkowe ograniczenia dla tabeli jeśli chodzi o wartości unikalne (oczywiście chodzi o podanie nazw kolumn lub ich kombinacji).

Mapowanie kolumn i ich pobieranie

Tabelę już mamy – co zatem z jej kolumnami? Do tej pory zadowalaliśmy się domyślnymi wartościami dostarczanymi przez Hibernate – łancuchy tekstowe lądowały w naszej bazie jako VARCHAR, identyfikator był longiem, zaś data była datą ;). Bardzo często takie podejście wystarcza – jednakże w przypadku zastanego schematu bazy danych, bądź też mając na uwadzę wydajność dobrze jest zabrać sprawy w swoje ręce. Do operowania budową i własnościami kolumny użytej do zapisywania wartości encji służy adnotacja (a jakże) – @Column. Posiada ona istotne atrybuty i dobrze jest się z nimi zapoznać – do najbardziej popularnych należą choćby: nullable, length czy unique.

Co zrobić w przypadku kiedy dane w tabeli są ‘ciężkie’? Załóżmy, że posiadamy kolumnę z danymi binarnymi (np. obrazek). Załóżmy, że mógłby być to dowolny obrazek (mający nawet kilka MB), który ma przedstawiać naszego herosa w przykładowej grze RPG. Pole w naszej encji Hero mogłoby wyglądać tak:

 @Lob
 @Column(columnDefinition="CLOB NULL")
 private String heroImage;

Jednakże zgódźmy się – z całą pewnością o wiele częściej będziemy wyświetlać proste wartości naszego herosa – level czy imię. Cały (duży) obrazek o wiele rzadziej. Cóż zatem z tym zrobić? Zastosować lazy loading. Domyślnie wszystkie pola encji (poza tymi oznaczonymi jako transient) są opatrzone ‘niejawnie’ adnotacją @Basic. Jednym z jej atrybutów jest fetch, który przyjmuje wartości zdefiniowane w typie FetchType. Domyślnie użyta wartość to EAGER. Oznacza ona natychmiastowe (zachłanne) pobieranie wartości. Jednakże pobieranie za każdym razem z bazy danych potencjalnie dużej ilości danych mija się z celem. Tutaj z pomocą przychodzi nam drugi typ pobierania danych – LAZY (leniwy czy tez opóźniony). Dzięki adnotowaniu pola by jego wartość była pobierana w ten sposób, zostanie ona pobrana z bazy danych dopiero kiedy kod źródłowy odwoła się do jej wartości. W przypadku naszego pola wyglądałoby to tak:

@Lob
@Column(columnDefinition="CLOB NULL")
@Basic(fetch=FetchType.LAZY)
private String heroImage;

Nasz przykład jest dość ekstremalny (pliki tego typu raczej przechowuje się fizycznie na dysku), ale dobrze pokazuje samą ideę. Do tematu lazy-loadingu powrócę podczas omawiania mapowania relacji i kolekcji.

Jakie konkretnie typy zmiennych możemy używać w encjach? Sporo 😉 Lista jest dość rozbudowana (cytat z książki):

• Primitive Java types: byte, int, short, long, boolean, char, float, double
• Wrapper classes of primitive Java types: Byte, Integer, Short, Long, Boolean,
Character, Float, Double
• Byte and character array types: byte[], Byte[], char[], Character[]
• Large numeric types: java.math.BigInteger, java.math.BigDecimal
• Strings: java.lang.String
• Java temporal types: java.util.Date, java.util.Calendar
• JDBC temporal types: java.sql.Date, java.sql.Time, java.sql.Timestamp
• Enumerated types: Any system or user-defined enumerated type
• Serializable objects: Any system or user-defined serializable type

Typy wyliczeniowe

W tym miejscu chciałbym napisać kilka słów o ENUMach. Przyznam szczerze, iż bardzo lubię tę konstrukcję – zarówno w przypadku Javy jak i baz danych (natywnie choćby wspierany przez MySQL). Z całą pewnością typy wyliczeniowe są o wiele bardziej czytelne niż maski bitowe bądź po prostu czyste wartości INT wrzucone do bazy. Oczywiście w przypadku JPA głównie chcielibyśmy poruszać się w obrębie obiektów, a do bazy danych nawet nie zaglądać, użycie typów wyliczeniowych jest bardziej przejrzyste (zainteresowanych odsyłam do Effective Java Joshui Blocha). Wspomniałem powyżej, że posiłkowanie się czystymi INTami nie jest sensowne. Jest to jednakże domyślny sposób, w jaki typy wyliczeniowe są przechowywane w bazie danych. Załóżmy, że chcielibyśmy przerobić naszego bohatera w ten sposób, aby jego profesja była właśnie typem wyliczeniowym. Stwórzmy zatem taki typ:

package com.wordpress.chlebik.jpa.domain;

public enum Profession {
    MAGE, WARRIOR, THIEF, DRUID, ARCHER
}

Oraz oczywiśie zmodyfikujmy klasę domenową naszego bohatera:

private Profession profession;

I od teraz możemy tworzyć naszego bohatera nadając mu jedną z dostępnych profesji. Co się stanie w przypadku zapisu obiektu do bazy danych? Otóż domyślnie kolejne wartości typu wyliczeniowego otrzymają wartości INT – zaczynając od zera. W 99% przypadków absolutnie nam to wystarczy, zwłaszcza jeśli nie musimy np. wyświetlać wartości tej zmiennej użytkownikowi. To ORM bierze na siebie kwestię przerobienia numerka na obiekt. Problem pojawi się, jeśli nagle postanowimy dodać do naszego typu wyliczeniowego kolejną profesję, ale wepchniemy ją między już istniejące! Spowoduje to, że np. wojownik mający do tej pory numerek 1 nagle dostanie 2. Logika naszej aplikacji totalnie rozjedzie się z bazą danych – w związku z tym lepiej takiej operacji nie przeprowadzać! Jeśli już to dopisujmy kolejne wartości ‘na końcu’ wszystkich wartości – problemy mamy z głowy.

Co jednakże zrobić jeśli cyferki nam nie starczą? Dla przykładu – nasz typ wyliczeniowy prawdopodobnie będzie dość często zmieniany (sensowność stosowania wówczas ENUMa zostawiam na boku). Wówczas najlepiej nie zdawać się na domyślne zachowanie JPA, ale należy poinstruować mechanizm jak ma się zachowywać. Wówczas możemy wykorzystać adnotację @Enumerated wraz z jej wartością @EnumType. Adnotacja @Enumerated przyjmuje domyślnie wartość EnumType.ORDINAL, czyli mapujemy wartości na liczby. Istnieje też druga opcja – EnumType.STRING. Nazwa jest dość czytelna – nasze wartości typu wyliczeniowego zostaną w bazie zapisane nie w kolumnie numerycznej, ale tekstowej. Od tej pory nie boli nas w jakikolwiek sposób kolejność zadeklarowanych wartości w typie, a także przeglądając bazę danych (grubym klientem dla przykładu) widzimy od razu w czytelnej formie jaką profesję mają zapisani gracze.

Adnotacje czasu

W pierwszym wpisie cyklu pojawiła się adnotacja @Temporal. Służy ona poinstruowania JPA w jaki sposób ma zapisać jeden z obiektów daty pochodzącego z pakietu java.util.*. Standardowo bowiem w Javie obiektami, które ‘dotykają’ bazy danych w przypadku dat i czasu są reprezentacje klas z pakietu java.sql.*java.sql.Date, java.sql.Time oraz java.sql.Timestamp. Jeśli własności naszej encji będą jednym z tych typów wówczas nie ma problemu – zostaną w pełni zrozumiane przez JPA. W przypadku klas java.util.Calendar oraz java.util.Date nie jest już tak różowo. Używając adnotacji @Temporal wraz z nadaniem jej wartości za pomocą typu wyliczeniowego TemporalType określamy który z typów z pakietu java.sql.* zostanie użyty do mapowania konkretnego pola ( DATE, TIME, TIMESTAMP ). W przypadku braku tej adnotacji (przynajmniej z użyciem Hibernate i bazy H2) został użyty typ TIMESTAMP.

Mapowanie klucza głównego

Wspomniałem wcześniej, że posiadanie klucza głównego jest jednym z sine qua non bycia encją. W związku z tym dość istotnym jest, aby jednoznaczne określenie indywidualnej było proste, ale też dość elastyczne. Najprostszym (zasadniczo też chyba najbardziej rozpowszechnionym) sposobem mapowania klucza głównego jest jednoznacznie unikalna identyfikacja encji za pomocą wartości liczbowej. Póki co to właśnie do takiego sposobu ograniczaliśmy nasz kod. Wspomniałem jednakże o elastyczności – JPA zapewnia ją poprzez możliwość wykorzystania w charakterze klucza następujących typów Javy:

• Primitive Java types: byte, int, short, long, char
• Wrapper classes of primitive Java types: Byte, Integer, Short, Long, Character
• String: java.lang.String
• Large numeric type: java.math.BigInteger
• Temporal types: java.util.Date, java.sql.Date

Możliwe jest też używanie float, double bądź ich wrapperów, ale nie jest to oczywiście zalecane.

W pierwszym wpisie generowania identyfikatorów nie dokonywaliśmy ręcznie. Jest to dość zrozumiałe – w istniejących bazach danych mamy mniej lub bardziej zaawansowane mechanizmy generowania kolejnych wartości dla różnych typów. Z definicji jest to o wiele bardziej racjonalne podejście niż ręczne zapewnianie unikalności identyfikatorów. By wykorzystać możliwość baz danych musimy użyć adnotacji @GeneratedValue – jak sama nazwa wskazuje generowanie adnotowanych nią własności przejmie JPA. Nasza adnotacja posiada dwa atrybuty – generator oraz strategy. Pierwszy z nich ma wartość tekstową i określa po prostu nazwę używanego generatora (czyli np. sekwencji w przypadku wyboru takiej strategii generowania). Atrybut strategy ( typu wyliczeniowego GenerationType ) wskazuje na sposób w jaki będą generowane kolejne identyfikatory. Oto ich lista:

  • AUTO
  • – domyślna wartość, która wskazuje na to, że to JPA (konkretnie dostawca implementacji) powinno zadecydować o użytej strategii. Nie jest to do końca bezpieczne rozwiązanie, gdyż provider może zdecydować o wykorzystaniu zasobów, do których np. może nie mieć uprawnień (jak choćby tworzenie tabeli). Najlepiej sprawdza się przy testach i prototypowaniu.

  • SEQUENCE
  • – zakłada użycie istniejącej sekwencji dla generowania kolejnych wartości. W tym momencie wchodzi do gry adnotacja @SequenceGenerator. Jedynym obowiązkowym atrybutem jest nazwa generatora, sama nazwa użytej sekwencji domyślnie jest ustawiana przez dostawcę implementacji. Zatem bohaterowie w naszej grze mogliby mieć tak generowane id:

    @SequenceGenerator(name="heroesPKGenerator", sequenceName="SEQ_HEROES_ID")
    @Id @GeneratedValue(generator="heroesPKGenerator")
    private int id;
    
  • IDENTITY
  • – jeśli baza danych umożliwia wykorzystanie specjalnego typu kolumny (np. w MySQL to kolumny oznaczone atrybutem AUTO-INCREMENT) wówczas zostanie ona użyta. Taką strategię użyłem w swoim przykładzie. Jej zaletą jest prostota i brak dodatkowych obostrzeń (np. możliwość tworzenia sekwencji). Problemem jest brak przenoszalności takiego rozwiązania między niektórymi bazami, a także brak dostępności wygenerowanego ID przed zakończeniem transakcji.

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    
  • TABLE
  • – zostanie stworzona oddzielna tabela w bazie danych dla przechowywania kolejnej dostępnej wartości dla klucza. Jest to zasadniczo najbardziej przenośna strategia – baza danych nie musi wspierać kolumn identyfikujących lub strategii, ale tworzenie tabel raczej tak 😉 Używając tej strategii tworzona jest oddzielna tabela, która zawiera dwie kolumny – w jednej podany jest identyfikator będący nazwą generatora, w drugiej zaś liczbowa wartość ostatnio użytego identyfikatora. W przypadku używania tej strategii najlepiej posiłkować się adnotacją @TableGenerator, której atrybuty umożliwiają sterowanie nazwą używanej tabeli, nazwami kolumn i szeregiem innych. Przykładowy kod mógłby wyglądać tak:

            @TableGenerator(
                name="heroesPKGenerator", 
                table="ID_GEN", 
                pkColumnName="GEN_KEY", 
                valueColumnName="GEN_VALUE", 
                pkColumnValue="ID", 
                allocationSize=1)
            @Id
            @GeneratedValue(strategy=TABLE, generator="heroesPKGenerator")
            private int id;
    

Obiekty wbudowane

Rzecz dotyczy bytów, które po angielsku nazywane są embedded objects. Są one obiektami, ale nie posiadają jednoznacznej identyfikacji – innymi słowy zaś – nie są encjami. Dane, które reprezentują są przechowywane w wierszu tabeli w bazie danych i jako takie stanowią składową encji. Myślę, że przykład z naszej gry RPG będzie bardziej wymowny. Nasza klasa bohatera zawiera kilka prostych wartości, które opisują postać. Jednakże wraz ze wzrostem ilości informacji rośnie nam stopień komplikacji encji jako obiektu Javy. Gdybym miał operować na obiekcie Javy, który posiada 20 składowych podrapałbym się po głowie i stwierdził krótko, że coś tutaj z software craftsmanship nie do końca po drodze. Temu właśnie służą embedded objects – do grupowania logicznie powiązanych właściwości.

Z całą pewnością gracze w RPG powinni posiadać jakąś gotówkę. Jednakże w realiach RPG płacenie za bułkę czystym złotem niespecjalnie ma sens. Moglibyśmy zatem wprowadzić rozróżnienie na 3 typy monet w zależności od kruszczu, z którego są wykonane. Mielibyśmy zatem monety miedziane, srebrne i złote.

private int cooper;
private int silver;
private int gold;

Klasa Hero już posiada dokładnie 5 własności. Dodanie tak prostej informacji jak ilość posiadanych pieniędzy niemal podwaja ich ilość. Jednocześnie zaś informacje o posiadanych środkach finansowych zostaną zapisane w bazie zaraz obok imienia czy profesji. Aby nasza klasa jako byt programistyczny wyglądała troszeczkę bardziej sensownie możemy zastosować klasę wbudowaną. Kod wyglądałby tak:

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

Klasę Hero obdarowujemy nową własnością.

@Embedded private FinanceState finance;

Zaś podczas tworzenia bohatera dorzucamy taki kod:

FinanceState finances = new FinanceState();
finances.setCooper(10);
finances.setSilver(20);
finances.setGold(1);
h.setFinance(finances);

Czyż nie jest czyściej? W dodatku do tego obiekty klasy FinanceState mogą posiadać użyteczne metody użytkowe (czy stać mnie na zakup towaru? czy mogę wymienić np. 20 sreber na 1 złoto?). Jeśli uruchomimy teraz naszą testową aplikację w bazie danych tabela Heroes zostanie rozszerzona o 3 kolumny z ilością posiadanych monet. Używanie obiektów wbudowanych ma jeszcze jedną zaletę – jeśli zdarzy się nam sytuacja, w której potrzebowalibyśmy podobnego rozwiązania (np. w naszej grze do zapisu informacji o przeciwnikach – ile monet jakiego typu będziemy mogli uzyskać kiedy takowego deilkwenta poślemy do piachu), klasę wbudowaną możemy bez problemu użyć raz jeszcze.

Reużywalność obiektów wbudowanych może napotkać pewne problemy. Wyobraźmy sobie dopiero co wspomnianą sytuację – nasz bohater posiada kolumny nazwane dokładnie tak samo jak własności w klasie FinanceState. Jednakże tabela z wrogami została dokoptowana z innego projektu/gry i konkretne kolumny mają nazwy skrócone – co, si, gl. Mamy problem. Jednakże da się go obejść za pomocą adnotacji @AttributeOverride oraz @AttributeOverrides. Umożliwiają one dla konkretnej klasy nadpisać wartości nazw kolumn obiektu wbudowanego. Przykładowo klasa reprezentująca konkretnego wroga problem finansów mogłaby rozwiązywać w ten sposób:

    @Embedded
    @AttributeOverrides({
	    @AttributeOverride(name="cooper", column=@Column(name="co")),
	    @AttributeOverride(name="silver", column=@Column(name="si")),
	    @AttributeOverride(name="gold", column=@Column(name="gl")),
    })
    private FinanceState finance;

To tyle na dziś. W następnym artykule przypatrzymy się bliżej relacjom.

Prosty projekt dla Hibernate jako JPA2

W poprzednim wpisie poświęconym certyfikacji JPA wspominałem o tym, że dla nauki najbardziej wygodnym podejściem jest korzystanie z Hibernate w aplikacji konsolowej. Postanowiłem dla ułatwienia zamieścić bardzo prosty projekt (w oparciu o Mavena), aby pomóc w rozpoczęciu pracy.

Tworzymy w naszym IDE projekt Mavenowy. Grupy, artefakty i wersje pozostawiam już do swobodnej interpretacji. Najważniejsze są wszystkie zależności. Mój plik POM wygląda tak (zbudowany na podstawie jednego z przykładów dostępnych w sieci, nie pomnę juz skąd dokładnie):

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.wordpress.chlebik</groupId>
 <artifactId>jpa</artifactId>
 <version>1.0.0</version>
 <packaging>jar</packaging>

 <properties>
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 <maven.compiler.source>1.7</maven.compiler.source>
 <maven.compiler.target>1.7</maven.compiler.target>

 <hibernate.version>4.1.2.Final</hibernate.version>
 <org.apache.derby.version>10.8.1.2</org.apache.derby.version>

 <log4j.version>1.2.16</log4j.version>
 <slf4j.version>1.6.4</slf4j.version>

 </properties>

 <repositories>
 <!-- Hibernate Repository -->
 <repository>
 <id>jboss</id>
 <name>JBoss Release Maven Repository</name>
 <url>https://repository.jboss.org/nexus/content/repositories/releases/</url>
 </repository>
 </repositories>

 <pluginRepositories>
 <pluginRepository>
 <id>jboss-public-repository-group</id>
 <name>JBoss Public Maven Repository</name>
 <url>https://repository.jboss.org/nexus/content/groups/public-jboss/</url>
 <snapshots>
 <enabled>true</enabled>
 </snapshots>
 <releases>
 <enabled>false</enabled>
 </releases>
 </pluginRepository>
 </pluginRepositories>

 <dependencies>

 <dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
 <version>1.2.145</version>
 </dependency>

 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-core</artifactId>
 <version>${hibernate.version}</version>
 </dependency>
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-entitymanager</artifactId>
 <version>${hibernate.version}</version>
 </dependency>
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-c3p0</artifactId>
 <version>${hibernate.version}</version>
 </dependency>
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-ehcache</artifactId>
 <version>${hibernate.version}</version>
 </dependency>
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-annotations</artifactId>
 <version>3.5.6-Final</version>
 </dependency>
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-validator</artifactId>
 <version>4.2.0.Final</version>
 </dependency>

 <dependency>
 <groupId>log4j</groupId>
 <artifactId>log4j</artifactId>
 <version>${log4j.version}</version>
 </dependency>
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-api</artifactId>
 <version>${slf4j.version}</version>
 </dependency>
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>jcl-over-slf4j</artifactId>
 <version>${slf4j.version}</version>
 </dependency>
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
 <version>${slf4j.version}</version>
 </dependency>

 <dependency>
 <groupId>commons-lang</groupId>
 <artifactId>commons-lang</artifactId>
 <version>2.6</version>
 </dependency>
 </dependencies>

</project>

Do poprawnego działania potrzebujemy jeszcze dwóch rzeczy – persistence.xml oraz kodu (głównej klasy uruchamiającej oraz klasy domenowej). Zaczniemy od persistence.xml (w folderze /main/src/resources/META-INF).


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence                                   http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="persistenceUnit" transaction-type="RESOURCE_LOCAL">
    
   <class>com.wordpress.chlebik.jpa.domain.Hero</class>
     
   <properties>
         <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
         <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />          
         <property name="javax.persistence.jdbc.user" value="sa" />
         <property name="javax.persistence.jdbc.password" value="" />

         <property name="hibernate.show_sql" value="true" />
         <property name="hibernate.hbm2ddl.auto" value="create" />
     </properties>
   
  </persistence-unit>
</persistence>


No i potem mamy klasę uruchomieniową:

package com.wordpress.chlebik.jpa;

import java.util.Date;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.wordpress.chlebik.jpa.domain.Hero;

public class App {
    
    public static void main(String... args) {

   EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory( "persistenceUnit" );
   EntityManager entityManager = entityManagerFactory.createEntityManager();

		entityManager.getTransaction().begin();
		Hero h = new Hero();
		h.setCreationDate(new Date());
		h.setLevel(1);
		h.setName("Chlebikowy Mag");
		entityManager.persist( h );

		entityManager.getTransaction().commit();
		entityManager.close();

    }
}

No i na koniec wspomniana klasa encji:

package com.wordpress.chlebik.jpa.domain;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.GenericGenerator;

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

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

    @Temporal(TemporalType.TIMESTAMP)
    private Date creationDate;

    private String name;
    private Integer level;

    public Hero() { }

    // Gettery i settery ucięte

}

Taki prosty szkielet programu umożliwi nam pracę z JPA opierając się na bazie danych H2 uruchamianej w pamięci za każdym startem naszej aplikacji. Kompilacja i uruchomienie aplikacji trwa mgnienie oka, zaś dzięki Mavenowi bardzo łatwo jest ew. podmienić dostawcę JPA czy szybko przerobić projekt na appkę webową uruchamianą w kontenerze JEE.

Dla ułatwienia sobie prac i nauki można dorzucić jeszcze dwie rzeczy – logowanie oraz podgląd bazy danych. Logowanie dodać można bardzo prosto – do folderu /src/main/resources należy dodać plik log4j.properties, u mnie wygląda on tak:

log4j.rootLogger=WARN, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%n%m%n%n

I dzięki temu w naszej głównej klasie możemy sobie utworzyć obiekt loggera:

private static final Log log = LogFactory.getLog(App.class);

Dzięki czemu można obyć się bez System.out.println. Drugą sprawą jest baza danych i ew. podgląd jej zawartości. Zasadniczo przy używaniu JPA nie powinniśmy w ogóle dotykać bazy danych jako takiej. Jednakże jest to raczej pobożne życzenie – bardzo często JPA jest wprowadzane do istniejącego już schematu bazy. Do tego dobrze jest czasem popatrzeć czy aspekty wydajnościowe (klucze przede wszystkim) zostały poprawnie przez JPA rozpoznane. Tak jak pisałem w przypadku powyższej aplikacji baza danych jest przechowywana w pamięci i jako taka jest usuwana po zakończeniu działania programu. Czasem jednakoż dobrze byłoby przyjrzeć się wygenerowanym tabelom, relacjom czy zapisanym danym. Możemy to osiągnąć uruchamiając bazę H2 jako oddzieny proces w trybie serwerowym. By to osiągnąć trzeba oczywiście zaciągnąć plik JAR z bazą H2. Myślę, że strona domowa bazy jest świetnym miejscem by ten plik uzyskać. Po jego ściągnięciu na dysk trzeba plik JAR uruchomić (w Windowsie wystarczy na niego kliknąć). Pojawi się mniej więcej takie okienko.

H2Console

Odpowiednie pola trzeba wypełnić dokładnie takimi samymi wartościami jak powyżej. Po zrobieniu tego dajemy ‘Połącz’ i w przeglądarce mamy już dostęp do naszej bazy. Teraz wystarczy zmienić plik persistence.xml w miejscu dotyczącym połączenia.

<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1" />

I każdorazowe uruchomienie naszej aplikacji będzie łączyć się do bazy danych, ale stworzone tabele/relacje/dane zostaną zapisane i będą dostępne do przeglądania dopóki nie wyłączymy procesu H2. Oczywiście baza danych musi być odpalona 😉

Encje i cały ten bałagan

Przyszedł czas by rozpocząć zabawę w certyfikację JPA. W dzisiejszym wpisie zamierzam przedstawić pokrótce podstawowe pojęcia i zagadnienia dotyczące mapowania ORM, encji i o co w ogóle chodzi z tymi pojęciami.

Encje

Podstawowym pojęciem w przypadku mapowania relacyjno-obiektowego oraz persystencji jest encja. Z pewnością spotkaliście się z tym pojęciem w wielu różnych znaczeniach. Nasza książeczka definicuje ją krótko i treściwie:

An entity is essentially a noun, or a grouping of state associated together as a single unit. It may participate in relationships to any number of other entities in a number of standard ways. In the object-oriented paradigm, we would add behavior to it and call it an object.

Jest to bardzo krótka i treściwa charakterystyka. Nasuwającym się pytaniem jest – jakie to cechy musi posiadać obiekt, aby uznać go za encję? Jest ich kilka:

  • Persistability – encje jako takie muszą mieć możliwość być zapisanymi w bazie danych.
  • Identity – obiekt, który chcemy uznać za encję musi być jednoznacznie identyfikowalny pomiędzy innymi podobnymi sobie obiektami. Jeśli bowiem mamy dokonywać operacji na obiekcie musimy mieć pewność, że dokonujemy ich na wybranym przez nas obiekcie, a nie pierwszym lepszym, który się nawinął. Dla nas oznacza to, że obiekt posiada unikalny klucz.
  • Transactionality – encje muszą być ‘transakcyjne’ (mam nadzieję, że to prawidłowe tłumaczenie) – móc istnieć w kontekście transakcji. Do transakcji wrócę później.
  • Granularity – słowo to można przetłumaczyć jako ‘ziarnistość’. Chodzi o to, aby encja była pełnoprawnym obiektem, który trzyma w sobie nie pojedynczą wartośc (o prymitywie nie wspomnę), ale kilkanaście cech/obiektów, które tworzą logiczną całość. Tworzenie encji do trzymania ID i jednego łańcucha tekstowego jest możliwe, ale raczej mało (najczęściej) sensowne.

Encja jako taka poza informacjami, które przechowuje ‘sama w sobie’ (czyli po prostu swoimi własnościami) może posiadać tzw. metadane. Opisują one w jaki sposób encja ma się zachować w środowisku JPA. Zasadniczo jak większość tego typu danych w Javie możemy zapisać je na dwa sposoby – adnotacjami bądź też za pomocą XMLa. Wszystkie adnotacje, o których tutaj mowa znajdują się w pakiecie javax.persistence.*. Plików XML obecnie głównie używa się do stworzenia pliku konfiguracyjnego dla JPApersistence.xml, który opisuje w jaki sposób będą zachowywały się nasze encje oraz dokładniejsze dane konfiguracyjne (najczęściej specyficzne dla dostawcy JPA). Oczywiście nic nie stoi na przeszkodzie, aby za ich pomocą również mapować encje. Jednakże od takiego podejścia się odchodzi (generuje duże ilości XMLa i rozbija informacje będące dla encji istotne na 2 pliki). W tutorialach oczywiście będę posługiwał się tylko adnotacjami (dla czystości kodu przede wszystkim).

Ok. Wiemy jakie cechy powinna mieć encja. Nie zbliżyliśmy się jednakże choćby odrobinę do stworzenia takowej z obiektu. Do tego zasadniczo potrzeba dwóch rzeczy:

  • adnotacji @Entity – adnotacja markerowa używana na poziomie klasy
  • adnotacji @Id – adnotacja markerowa używana na poziomie własności obiektu (lub metody dostępowej – o tym później).

Trochę wyprzedzając temat – brak adnotacji @Entity przy wpisaniu klasy jako potencjalnej encji w pliku konfiguracyjnym skutkuje błędem czasu wykonania (np. przy próbie zapisu encji). Brak adnotacji @Id z kolei zaowocuje wyjątkiem podczas wykonania (u mnie wyleciał org.hibernate.AnnotationException docelowo opakowanym w javax.persistence.PersistenceException). Zasadniczo wszystkie błędy niższego poziomu docelowo są wrapowane w PersistenceException.

Spójrzmy zatem na przykładową encję. Dodam znów na marginesie – modelem, którego będziemy używać jest gra RPG – będziemy zatem mieli encję reprezentującą gracza, jego cechy, posiadane przedmioty i co tam mi jeszcze do głowy przyjdzie. Zakładam, że będzie to troszkę bardziej przemawiające niż po raz milionowy przykłady typu User, Phone czy Address.

package com.wordpress.chlebik.jpa.domain;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.GenericGenerator;

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

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

    @Temporal(TemporalType.TIMESTAMP)
    private Date creationDate;

    private String name;
    private Integer level;

    public Hero() { }

    public Long getId() {
		return id;
    }

    private void setId(Long id) {
		this.id = id;
    }

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getLevel() {
		return level;
	}

	public void setLevel(Integer level) {
		this.level = level;
	}

	public Date getCreationDate() {
		return creationDate;
	}

	public void setCreationDate(Date creationDate) {
		this.creationDate = creationDate;
	}
}

Konkretnymi adnotacjami sugeruję na razie się nie przejmować (wrócę do tego). Jak widać nasza encja jest prostym POJOsem z dodatkami. Pytanie powinno brzmieć – gdzie i w jaki sposób taki obiekt zostanie zapisany w bazie danych? Zasada jest prosta – o ile nie jest ustawione inaczej wszystkie własności obiektu zostaną zapisane do bazy danych. Niezależnie od tego czy istnieją akcesory! Gdzie zapisane? Ano tutaj warto przyjrzeć się dokładniej adnotacji @Table – posiada ona atrybut o nazwie (jakże inaczej) – name. Wskazuje on na nazwę tabeli w bazie danych. Jeżeli atrybutu tego brak – wówczas w charakterze nazwy tabeli zostanie użyta nazwa encji.

EntityManager

Sama encja jest bardzo fajnym wynalazkiem. Zgódźmy się jednak – sama to ze sobą nic nie zrobi. Tutaj na scenę wkracza kolejne pojęcie-klucz – EntityManager. Jest to interfejs, który dostarcza funkcjonalności, dzięki którym w ogóle możemy mówić o operacjach bazodanowych (wyszukiwanie, zapisywanie i wszystko inne). Konkretne implementacje EntityManagera pobieramy korzystając z innego interfejsu o nazwie EntityManagerFactory. Zaś konkretne EntityManagerFactory są jednoznacznie powiązane z pojęciem PersistenceUnit. Jest to zdefiniowana w pliku persistence.xml ‘jednostka’, w której określamy klasy, które będą encjami w ramach PersistenceContext dostarczanego przez EntityManagera, a także właściwości połączenia do bazy danych. Rozumiecie? Pewnie że nie, ja też bym w tym momencie nie zrozumiał 😉

Poniżej mały rysunek.

Persistence UML

Zaś dalej sensownym do przeczytania wydaje się być materiał, do którego linkowałem już w pierwszym wpisie o JPA. EntityManagerFactory – obiekt bez którego ani rusz – w Javie SE otrzymujemy po prostu korzystając ze statycznej metody klasy Persistence. Kiedy mamy fabrykę wyciągnięcie EntityManagera jest trywialne:

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory( "persistenceUnit" );
EntityManager entityManager = entityManagerFactory.createEntityManager();

Tadam! Mamy EntityManagera i możemy ruszać na podbój świata 😉 Poza podbojem możemy też zająć się bardziej przyziemnymi rzeczami:

  • zapisywaniem encji w bazie (INSERT) – w taki oto choćby sposób:
    Hero h = new Hero();
    h.setCreationDate(new Date());
    h.setLevel(1);
    h.setName("Chlebikowy Mag");
    entityManager.persist( h );
    
  • wyszukaniem encji w bazie (SELECT) – posiłkując się kluczem głównym oraz typem encji:
    Hero h = entityManager.find(Hero.class, 1L);
    
  • usuwaniem encji (DELETE) – z małą pomocą wyszukiwania:
    Hero h = entityManager.find(Hero.class, 1L);
    entityManager.remove(h);
    
  • zmianą encji (UPDATE) – o dziwo bez konieczności używania EntityManagera do przeprowadzenia aktualizacji:
    Hero h = entityManager.find(Hero.class, 1L);
    h.setName("Chlebikowy wojownik");
    

Proste prawda? Aż miło popatrzeć jak odwieczne ‘SELECT * FROM’ i insze klony odchodzą powoli do lamusa.

Transakcje i SQL

By zakończyć temat ciekawych zastępników odniosę się jeszcze do transakcji i zapytań. Zasadniczo omawiany przeze mnie przykład dotyczy JPA uruchamianego poza środowiskiem EE (np. z aplikacji ‘konsolowej’ czy w Tomcacie). Używanie JPA w ramach serwera aplikacyjnego będzie poruszane w odpowiednich momentach (w aplikacjach JEE EntityManagera po prostu wstrzykujemy używając adnotacji), jednakże dla nauki łatwiej jest posiłkować się najprostszą możliwą aplikacją (głównie chodzi o szybkość uruchamiania). Wspominam o tym dlatego, iż w przypadku takich aplikacji wszystkie operacje zmieniające dane w bazie wymagają ‘wrapowania’ w transakcję. By nie zaciemniać zagadnienia (do transakcji powrócę w następnych wpisach) kawałeczek kodu jak powinny wyglądać przedstawione powyżej operacje (CUD).

entityManager.getTransaction().begin();

Hero h = new Hero();
h.setCreationDate(new Date());
h.setLevel(1);
h.setName("Chlebikowy Mag");
entityManager.persist( h );

entityManager.getTransaction().commit();

Do tej pory używaliśmy metod EntityManagera by osiągnąć zamierzony cel. W codziennej praktyce programistycznej pojawiają się często sytuacje, w których samo find nie wystarczy. Co zrobić kiedy potrzebujemy wykonać troszeczkę bardziej skomplikowane zapytanie? Z pomocą przychodzą nam wówczas również metody EntityManagera, które pozwalają na tworzenie obiektów zapytań. Reprezentowane są one przez dwa podstawowe interfejsy – TypedQuery oraz Query. Przykład tego pierwszego poniżej:

TypedQuery query = entityManager.createQuery("SELECT h FROM Hero h", Hero.class);
List heroes = query.getResultList();

Plik konfiguracyjny persistence.xml

Do tej pory głównie skupiałem się na encjach oraz pobocznym kodzie. O konfiguracji samego JPA wspomniałem niejako mimochodem. Prawda jednak jest taka, iż istnienie pliku persistence.xml jest warunkiem koniecznym dla poprawnego działania całej powyższej magii. Plik ten znajduje się w folderze META-INF (w projektach Mavena folder ten powinien leżeć w /src/main/resources). Mój dość podstawowy plik (z konfiguracją bazy danych H2 dla prostoty uruchamiania i działania) wygląda następująco:

<!--?xml version="1.0" encoding="UTF-8"?-->


 com.wordpress.chlebik.jpa.domain.Hero





 

Najistotniejszymi elementami jest nazwa naszego persistence-unit (używa się jej do stworzenia instancji EntityManagerFactory), lista klas encyjnych oraz podstawowe własności dla połączenia. Przed specyfikacją JPA w wersji 2 nazwy własności bywały różne (w zależności od bazy danych), co powodowało zrozumiałe problemy. Obecnie gdzie tylko to możliwe nazwy te są zestandaryzowane jak tylko się da.

Oracle Certified Expert, Java EE 6 Java Persistence API Developer – Here I come!

Jak wspomniałem w niedawnym wpisie zabrałem się do kolejnej certyfikacji. Trochę się zmieniło od czasów kiedy zdawałem SCJP, ba, Sun zniknął wykupiony przez Oracle. W sumie wyszło mi to na dobre – interesujący mnie egzamin to wprowadzony nie aż tak dawno temu Oracle Certified Expert, Java EE 6 Java Persistence API Developer.

Dlaczego ten certyfikat? Kontynuowanie dalszego zgłębiania się w meandry samej Javy na poziomie certyfikacyjnym niespecjalnie jest dla mnie korzystne. Oczywiście zawsze lepiej jest wiedzieć więcej niż mniej, jednakże w sytuacji ograniczonego czasu lepiej skupić się na czymś, co może pomóc na co dzień w pracy zawodowej. Swego czasu myślałem o certyfikacji z webserwisów. Po przejrzeniu materiałów dostępnych w sieci pomysły certyfikacji w webserwisach odłożyłem na półkę 😉 . Certyfikat webdevelopmentu uważam za zasadniczo niepotrzebny – samą książkę przygotowującą do niego (z serii Head First) czytałem dawno temu kiedy w ogóle poznawałem Javę. Dzisiaj w dobie używania frameworków webowych podstawowa znajomość servletów i JSP absolutnie wystarcza by wiedzieć co z czym – w razie potrzeby można zajrzeć do książki (uuups, okazało się, że podpiera ona mój monitor 😉 . Z certyfikatem dotyczącym całego JEE sprawa jest jeszcze bardziej skomplikowana – obejmuje bardzo duży zakres wiedzy, niespecjalnie póki co przydatnej dla mnie w codziennej pracy.

Inne certyfikaty najczęściej wymagają zrealizowania kursów Oracle, ergo, wydania dużych pieniędzy. Na placu boju pozostał zatem wykrojony z certyfikacji biznesowej egzamin poświęcony JPA. Kilka miesięcy temu miałem okazję ostro przysiąść z tą technologią. Czasem braki w teoretycznej wiedzy (specyfikacja you fool!) doprowadzały mnie wtedy do szału. W sumie przyszedł też czas na ponowne rozruszanie bloga – wipsy dotyczące SCJP póki co cieszą się wciąż bardzo wysoką oglądalnością.

Od czego zacząć? Myślę, że od wymagań. Znaleźć je można na stronie Oracle (miejmy nadzieję, że nie zmienią tego adresu, strony Oracla to największe badziewie jakie widziałem). Na pewno przyda się również podstawowa broń każdego podchodzącego do certyfikacji – specyfikacja JPA. Z całą pewnością wypada odwiedzić na początek stronę JavaRanch poświęconą właśnie temu egzaminowi. Znaleźć tam można całą masę ciekawych wątków, między innymi z czego się uczyć (podstawą jest książką Pro JPA 2 i specyfikacja), jak poszło innym zdającym, ciekawe są również wątki o symulatorach egzaminów. Na sam koniec zostawiłem kilka ciekawych źródeł w internecie (listę będę uzupełniał jak tylko znajdę coś ciekawego):

  • JPA Concepts – cholernie czytelne wprowadzenie do słownictwa JPA. Jeśli do tej pory nie do końca rozumiesz różnicę między PersistenceContext, a EntityManagerem to jest to świetne miejsce by zacząć.
  • JPA Quiz – zebrane w jednym miejscu kilkaset krótkich pytań i odpowiedzi dotyczących JPA.
  • JPA Mappings – ciekawa ni to appka, ni to wiki poświęcona mapowaniu relacji.
  • JPA Tutorial – skrócona wersja tego, co ja zamierzam zaprezentować w formie całego cyklu
  • Oracle Docs – strona dokumentacji Oracle poświęcona persystencji

Myślę, że formuła wpisów będzie troszeczkę inna niż w przypadku SCJP – książka Pro JPA2 nie jest oficjalnym podręcznikiem egzaminacyjnym zatem nie zawiera pytań na końcach rozdziałów. Postaram się zatem tworzyć streszczenia rozdziałów wraz z przykładami kodu (gdzie ma to sens), a także dorzucać jakieś ciekawe pytania i problemy przedstawiane we wpisach. Jednakże to się jeszcze okaże. Mój plan jest taki, aby w przykładach i efektywnym kodowaniu używać Hibernate – z moich dotychczasowych doświadczeń z dostawcami JPA ten jako jedyny nie pozostawił wielkiego WTF 😉 That’s all folks.

Działający przykład JEE w akcji

Jakoś tak się złożyło, że poza servletami i JPA niespecjalnie miałem w życiu pobawić się w EJB i insze wynalazki. Fakt, że pojawił się kiedyś dawno Spring skutecznie zniechęcał do posiłkowania się JEE w codziennym developmencie. Jednakże sytuacja zmieniła się wraz z wydaniem wersji 6 Javy EE.

Szukając w sieci materiałów dla przygotowań do certyfikatu Java Persistence API Developer Certified Expert (tak tak, pierwsze wpisy z przygotowań już niedługo) znalazłem dość ciekawy tutorial, który umożliwia postawienie w pełni funkcjonalnej aplikacji, na której możnaby przećwiczyć mniej i bardziej zaawansowane tematy związane z tą certyfikacją.

JBoss to nazwa budząca respekt. Jako firma oferuje szeroki wachlarz produktów – począwszy od serwera aplikacyjnego skończywszy na IDE. Oferuje również strasznie fajny tutorial znajdujący się  dokładnie pod tym adresem. Obejmuje on instalację dedykowanego IDE oraz przedstawia JEE w akcji – mamy i usługi sieciowe, mamy JSF, mamy też podpięte Hibernate jako ORM. Nic tylko brać i działać. Jeśli ktoś potrzebuje zobaczyć jak może wyglądać sensowna appka bez miliona zależności w POMie to powyższy adres jest świetnym punktem wyjścia.

SCJPTester wyświetla newsy – podstawy Springa w praktyce

Dziś przyszedł czas na coś większego. Mam nadzieję, że ilość kodu nie będzie odstraszająca. Ponieważ dziś zaimplementujemy coś z pomocą tandemu Spring + Hibernate. Konkretnie przebudujemy troszeczkę widok strony głównej po to, aby móc wyświetlić ostatnie informacje o aplikacji (podobną rzecz robiliśmy z ProgramBash). Jest to o tyle ciekawe, iż użyjemy Springa w całej jego krasie, stworzymy pierwszą tabelę w naszej bazie, nauczymy się czegoś więcej o kontrolerach, używaniu Hibernate poprzez klasy Springa, a ostatecznie troszeczkę podlejemy to sosem od FreeMarkera. Do dzieła.

Zaczniemy od bazy. Zainstalowałem ją jakiś czas temu, a do naszej aplikacji podpiąłem chwilkę później. W ten sposób wyposażeni możemy stworzyć pierwszą tabelę w bazie danych – konkretnie news:

CREATE TABLE news (
id serial PRIMARY KEY,
title varchar(127) NOT NULL,
content text NOT NULL,
data date NOT NULL
) WITHOUT OIDS ;

Co zaowocuje takim komunikatem:

NOTICE: CREATE TABLE will create implicit sequence "news_id_seq" for serial column "news.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "news_pkey" for table "news"
Zapytanie zostało wykonane w 454 ms i nie zwróciło żadnych wyników.

Mówiąc krótko – została utworzona sekwencja, która została przypisana do kolumny z naszym kluczem głównym (ten też został zresztą utworzony jak widać). Mamy zatem tabelę, na której będziemy pracować – dodałem do niej też 2 rekordy, aby mieć jakieś dane do pracy. Zajmiemy się teraz samym Springiem. Na czym bowiem polega jego zajefajność? Otóż podstawą jest właśnie wstrzykiwanie zależności – mamy obiekt, którego składową jest inny obiekt. Konfigurujemy te 2 obiekty w pliku XML, a Spring zajmie się utworzeniem obiektu, jego przekazaniem i wszystkim po drodze. Jest to o tyle praktyczne, iż redukuje ilość kodu, który trzeba za każdym razem napisać. Deklarujemy po prostu, że potrzebujemy w danym obiekcie innego obiektu i viola – działa. Suchy opis jednakże niewiele znaczy, zatem lepiej przerobić rzecz w praktyce.

Zaczniemy od klasy domenowej dla naszego newsa. Dla tej i pozostałych klas domenowych utworzymy oddzielny pakiet com.wordpress.chlebik.domain. Kod wygląda tak:

package com.wordpress.chlebik.domain;

import java.io.Serializable;
import java.sql.Date;

/**
 * Klasa domenowa dla newsa w serwisie
 *
 * @author chlebik
 */
public class News implements Serializable {

 private    long      id;
 private    String   title;
 private    String   content;
 private    Date     data;

 /**
 * Konstruktor
 *
 * @author chlebik
 */
 public News() {}

 // Gettery
 public long getId() {
 return id;
 }

 public String getTitle() {
 return title;
 }

 public String getContent() {
 return content;
 }

 public Date getData() {
 return data;
 }

 // Settery
 public void setId( int id ) {
 this.id = id;
 }

 public void setTitle( String title ) {
 this.title = title;
 }

 public void setContent( String content ) {
 this.content = content;
 }

 public void setData( Date data ) {
 this.data = data;
 }

}

Nihil novi sub sole. Klasa jest prosta i sztampowa do bólu. Mapowanie:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
 <class name="com.wordpress.chlebik.domain.News" table="news">
 <id column="id" name="id" type="int">
 <generator/>
 </id>
 <!--  zwykle kolumny -->
 <property column="content" name="content" type="string"/>
 <property column="title" name="title" type="string"/>
 <property column="data" name="data" type="date"/>
 </class>
</hibernate-mapping>

Należy pamiętać o dopisaniu mapowania do pliku konfiguracyjnego Hibernate. Teraz wypadałoby zaimplementować klasę dostępu do naszej klasy domenowej, czyli mówiąc krótko piszemy DAO. Rzecz jasna wychodzimy od interfejsu, który zdefiniuje kontrakt jaki chcemy by spełniały ew. klasy implementujące. Uwaga! Dla tych co nie wiedzą dlaczego używamy interfejsów ważne wskazanie – posiadając interfejs korzystamy przy ewentualnej pracy z polimorfizmu. Zatem nagłe przepisanie aplikacji z MySQLa na PostgreSQL i na odwrót nie stanowi problemu, gdyż ew. obiekty przekazujemy rzutując w górę na interfejs.

Rzecz jasna w przypadku newsów na stronie głównej problemu nie ma – będzie tam póki co jedna metoda, która będzie nam wyciągała nasze newsy w określonej ilości. Może i dla takiego zastosowania możnaby od razu sklepać klasę, ale ani to profesjonalne, ani nie wykształca dobrych zachowań. Zatem tworzymy prosty interfejs w pakiecie com.wordpress.chlebik.dao.interfaces.

package com.wordpress.chlebik.dao.interfaces;

import com.wordpress.chlebik.domain.News;
import java.util.List;

/**
 * Interfejs specyfikujacy kontrakt dla klas DAO newsow w serwisie
 *
 * @author chlebik
 */
public interface NewsDaoInterface {
 public List<News> getNews( int counter );
}

Interfejs mamy – teraz przyszedł czas na klasę go implementującą. I tutaj pierwszy styk Springa z Hibernate. Ten pierwszy posiada gotowe klasy, które służą obsłudze zapytań SQLa poprzez Hibernate. Oczywiście skorzystamy z tej możliwości, tym bardziej, że ułatwia to pisanie kodu. Jak bowiem wyglądałaby nasza klasa DAO? Zaimplementowalibyśmy metodę, w której wyciągalibyśmy sesję, potem ją zamykali, obsługa wyjątków, blech, coś brzydkiego, prawie jak JDBC. Zaczniemy od konfiguracji – musimy poinformować Springa, że używać będziemy Hibernate. Jednakże wprowadzimy pewien porządek, coby nie mieszać niepotrzebnie w dotychczasowym pliku konfiguracyjnym. Stworzymy oddzielny plik XMLa z konfiguracją bazy danych, a następnie zaimportujemy go do obecnie już istniejącego frontcontroller-servlet.xml. Nasz plik nazwiemy database-config.xml i wrzucamy do katalogu WEB-INF:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

 <bean id="dataSource">

 <property name="driverClassName">
 <value>org.postgresql.Driver</value>
 </property>
 <property name="url">
 <value>jdbc:postgresql://88.198.31.169/chlebik</value>
 </property>

 <property name="username"><value>MOJ_USER</value></property>
 <property name="password"><value>MOJE_HASLO</value></property>
 </bean>

 <bean id="mySessionFactory">

 <property name="mappingResources">
 <list>
 <value>News.hbm.xml</value>
 </list>
 </property>

 <property name="hibernateProperties">
 <props>
 <prop key="hibernate.dialect">
 org.hibernate.dialect.PostgreSQLDialect
 </prop>
 </props>

 </property>
 <property name="dataSource">
 <ref bean="dataSource"/>
 </property>

 </bean>
</beans>

Zaś do pliku frontcontroller-servlet.xml dopisujemy taki kod (dotyczy bazy danych jak i naszego beana z newsami):

<import resource="database-config.xml"/>

  <bean id="newsDao" class="com.wordpress.chlebik.dao.implementation.NewsDao">
        <property name="sessionFactory" ref="mySessionFactory" />
     </bean>

  <bean name="/index.html" class="com.wordpress.chlebik.controllers.IndexController">
       <property name="newsDao" ref="newsDao" />
  </bean>

Mamy pokonfigurowane stosowne beany by ogarnąć połączenie z bazą danych. W ten sposób wstępnie poinformowaliśmy Springa (zdefiniowaliśmy bean) o istnieniu fabryki sesji Hibernate. Ślicznie. Dzięki temu możemy teraz zastanowić się nad implementacją klasy DAO dla newsów. Mamy tutaj kilka możliwości:

  • HibernateDaoSupport – jest to klasa, którą powinny rozszerzać nasze klasy implementujące interfejs DAO konkretnej encji. Dzięki jej użyciu uzyskujemy o wiele łatwiejszy dostęp do sesji (po to właśnie skonfigurowaliśmy session factory), co prawda może to momentami być problematyczne ( o tym pewnie kiedy indziej napiszę ), dla obecnego przykładu powinno być OK.
  • HibernateDaoSupport wraz z obiektem pomocniczym HibernateTemplate – jest pokłosiem powyższego – również rozszerzamy klasę HibernateDaoSupport, ale za to odowłujemy się również do obiektu pomocniczego HibernateTemplate, co znacznie skraca kod.

Jako, że jedno wynika z drugiego pokażę obie implementacje. W wersji pierwszej po prostu rozszerzymy klasę HibernateDaoSupport:


package com.wordpress.chlebik.dao.implementation;

import com.wordpress.chlebik.dao.interfaces.NewsDaoInterface;
import com.wordpress.chlebik.domain.News;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

/**
 * Klasa DAO dla newsów
 *
 * @author chlebik
 */
public class NewsDao extends HibernateDaoSupport implements NewsDaoInterface {

 List<News> newsy;
 SessionFactory sessionFactory;

 /**
 * Bezargumentowy konstruktor
 */
 public NewsDao() {}

 /**
 * Wyciaga newsy w zadanej ilosci
 *
 * @param   int    counter
 * @return  List<News>
 */
 public List<News> getNews(int counter) {

 try {

 Session session = getSession();
 newsy = (List<News>)session.createQuery( "from News order by data desc limit " + counter ).list();
 releaseSession( session );
 return newsy;

 } catch ( Exception e ) {
 System.out.println( e.toString() );
 }

 return newsy;

 }

 public List<News> getNewsy()  {
 return newsy;
 }

 public void  setNewsy( List<News> lista )  {
 newsy = lista;
 }
}

Jak widać łatwiej dostać się do sesji, łatwiej ją uwolnić i w ogóle jest zawsze czysto, zawsze sucho i powiedzmy, że zazwyczaj pewnie. Jednakże kod ten można jeszcze bardziej uprościć i doprowadzić do takiej oto postaci (sama metoda getNews()):

public List<News> getNews(int counter) {
         return (List<News>) getHibernateTemplate().find( "from News order by data desc limit " + counter );
}

Obiekt pomocniczny HibernateTemplate udostępnia kilka najbardziej popularnych metod używanych przy pracach z bazami danych, dlatego też w tak prostych przypadkach jego użycie jest jak najbardziej wskazane. Po więcej informacji na temat tej klasy odsyłam do dokumentacji. Ja użyłem metody find, która jak widać wyciąga rekordy pasujące do zapytania. Uwaga! Używanie tego podejścia jest bardzo szybkie i przyjemne, ale automatycznie mocno skazuje nas na integrację ze Springiem. W przypadku gdybyśmy chcieli zmienić coś w samym kodzie aplikacji i ewentualnie przemigrować jego kawałek na inny framework mielibyśmy trochę roboty. Polecam odnośnik z jednego z komentarzy do tutoriala Darka Zonia ( mały link ).

Cóż nam teraz zostało? Sprawdzić czy w ogóle coś działa! Odwołamy się do istniejącego już kontrolera – IndexController. Tam w metodzie handleRequest do tej pory zwracaliśmy prostą instancję ModelandView. Na razie zostawimy ten temat sam sobie, natomiast stworzymy obiekt DAO i zobaczymy czy na pewno zwrócił to, co powinien zwrócić. Modyfikujemy zatem ww. metodę i dodajemy taki oto kod:


newsy = newsDao.getNews(MAIN_PAGE_NEWS_COUNTER);
System.out.println( "Zwroconych newsow " + newsy.size() );

Jak widać do kontrolera dodałem też stałą, która będzie nam wskazywała ile newsów pobrać na główną stronę. W codziennej praktyce rzeczy związane z konkretnym kontrolerem/akcją umieszczam najbliżej jak się da – do tego jako stałą. Jakoś tak bardziej to elegancko wygląda. Choć pewnie w komercyjnym projekcie dla zewnętrznego klienta takie rzeczy powinno się trzymać w zewnętrznym pliku, a konto łatwiejszego utrzymania. Uruchamiamy nasz projekt. I w konsoli wyskakuje krótkie:


Zwroconych newsow 2

Czyli coś poszło tak jak powinno 😉  Oczywiście kwestia jest teraz taka, że wypadałoby owe newsy przedstawić w odpowiedni sposób. Rzecz jasna wpierw trzeba listę z newsami przekazać do widoku. W kontrolerze zatem zamieniamy linijkę z instrukcją zwrotu obiektu ModelandView na następującą:


return new ModelAndView( "index", "newsList", newsy );

I po kilku zabawach z CSSem oraz z modyfikacją widoku:

  <#list newsList as News>
                  <div class="col">
                      <h4 style="color: #FFF2B3;">${News.title}</h4>
                      <h5>${News.data}</h5>
                      <p style="font-size: 10px; line-height: 1.2em;">${News.content}</p>

                  </div>
    </#list>

Znaczniki są dość samo-opisujące. Mamy zwykłą iterację po liście i ostateczny efekt wygląda w taki oto sposób:

Mavenizacji ciąg dalszy – dlaczego tym razem wyszło

Dziś znów na tapecie Maven. Nie tak dawno pisałem jak poszły moje pierwsze boje z tym narzędziem. W skrócie – nie poszły.

Zakończyłem jednakże prace z szkieletem projektu i po chwili namysłu (plus fakt, że poszedłem spać przed 3) stwierdziłem, że może to i dobrze. Jak wspominałem w poprzednim wpisie w dokumentacji Springa jest opisane tworzenie projektu krok po kroku, wraz z dodawaniem katalogów i takich tam. Stwierdziłem zatem, że jeśli już poznawać nowe narzędzie to od samych podstaw, a do takowych na pewno będzie należało klepanie wszystkiego z palca. Przypomina to trochę poznawanie linuxa od instalowania Gentoo, ale co tam. Utworzony projekt wygląda na razie bardzo skromnie:

Nie ma tego wiele. Podpięte mamy tylko API do serwletów oraz JUnit i to jeszcze nie w wersji 4. Widok w IDE rzecz jasna nie odzwierciedla sytuacji na dysku – tak projekt wygląda od strony plików i katalogów:

Tutaj wiemy już coś więcej. W katalogu src będzie trzymany kod – zarówno ten “podstawowy”, jak i pliki związane z prezentacją w sieci jak np. CSS czy obrazki. Znajdziemy też wstępnie katalog do trzymania testów, a także przygotowany katalog dla plików wygenerowanych i gotowych do wdrożenia na serwerze. Sugeruję zajrzeć do pliku POM.xml, który jest podstawowym plikiem dla Mavena. Na razie jest on dość prosty i krótki:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.wordpress.chlebik</groupId>
 <artifactId>scjpsym</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>scjpsym</name>
 <url>http://maven.apache.org</url>

 <dependencies>

 <dependency>
 <groupId>javax.servlet</groupId>
 <artifactId>servlet-api</artifactId>
 <version>2.5</version>
 <scope>provided</scope>
 </dependency>

 <dependency>
 <groupId>javax.servlet.jsp</groupId>
 <artifactId>jsp-api</artifactId>
 <version>2.1</version>
 <scope>provided</scope>
 </dependency>

 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>3.8.1</version>
 <scope>test</scope>
 </dependency>

 </dependencies>
 <build>
 <plugins>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>2.0.2</version>
 <configuration>
 <source>1.5</source>
 <target>1.5</target>
 </configuration>
 </plugin>
 </plugins>
 <finalName>scjpsym</finalName>
 </build>
 <properties>
 <netbeans.hint.deploy.server>Tomcat60</netbeans.hint.deploy.server>
 </properties>
</project>

Dość istotna jest ostatnia część pliku, a mianowicie plugin maven-compiler-plugin. Domyśłnie Maven kompiluje kod zgodny z Javą w wersji bodajże 1.3, co jest zasadniczo prehistorią. Dlatego też określenie docelowej wersji na 1.5. Powiedzmy, że na razie taki plik nam starcza. Jednakże aplikacja ma za zadanie działać ze Springiem, Hibernate i Spring MVC. Tutaj dochodzimy do sedna sprawy – zależności. W powyższym przykładzie element dependencies zawiera w sobie opisy pojedynczych bibliotek wraz z ich wersjami. Co więcej – dodanie bibliotek spowoduje przy budowaniu projektu próbę zainstalowania (w sensie dodania do projektu) bibliotek potrzebnych by pracowały te wskazane przez nas. Są to tzw. zależności przechodnie i każdy kto swego czasu pracował z linuxami opartymi na pakietach RPM doceni jak miłą rzeczą jest brak konieczności martwienia się o zależności.

Mała uwaga! Standardowo aplikacje tworzę w środowisku Windows. Zakładam rzecz jasna, że czytelnicy mojego bloga wiedzą, że kiedy napiszę dla przykładu, iż należy wywołać komendę mvn install to albo zrobią to z poziomu konsoli, albo np. Cygwina. O konieczności dopisania lokalizacji plików wykonywalnych Mavena do zmiennych systemowych chyba nie muszę wspominać.

Na początek powiedzmy, że chcemy poprawić naszą wersję JUnita. Trochę nie bardzo wygląda wersja 3.8.1, kiedy kilka tygodni temu wyszła wersja o 1 oczko “wyższa”. Edytujemy zatem plik POM.xml i zmieniamy numerek wersji na 4.8.1. Teraz wystarczy dać PPM na zakładce Test Libraries i wybrać opcję Download missing dependencies. Mamy nowego JUnita.

Zajmiemy się teraz Springiem. Postanowiłem używać wersji 2.5.6, gdyż najnowsza (trzecia) została wydana raptem w lutym bodajże, dlatego też nie jest dla mnie na tyle “wiarygodna” by pisać w niej edukacyjną aplikację (w sensie pierwszej styczności z frameworkiem). Do tego dorzucimy rzecz jasna Spring MVC. Takie zależności dopisujemy do odpowiedniego miejsca w POM.xml.

 <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring</artifactId>
    <version>2.5.6</version>
    <type>jar</type>
    <scope>compile</scope>
 </dependency>

 <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>2.5.6</version>
    <type>jar</type>
    <scope>compile</scope>
 </dependency>

Próba zbudowania projektu w tej chwili po prostu się nie powiedzie. Dlatego choćby, że nasze biblioteki nie zostały bynajmniej ściągnięte. Prawy przycisk myszy na zakładce Libraries i bodajże wybieramy Download source (nie jestem w stanie powtórzyć sytuacji, kiedy nie mam źródeł na kompie, nawet przy nowym projekcie). Jednakże zasadniczo przy dodawaniu zależności należy obserwować widok naszego projektu, gdyż NetBeans wszystko startuje od razu. Grzebiąc dalej trzeba by dodać Hibernate i tutaj zrobimy inaczej. Prawy przycisk myszy na zakładce libraries i wybieramy AddDependency. Pola na górze przeznaczone dla id artefaktu i grupy są podpięte pod autocompleter – zatem spokojnie można wpisać tam org.hi i na liście powinniśmy zostać już tylko z Hibernate. Ostatecznie po dodaniu wpis w pliku konfiguracyjnym wygląda tak:

  <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate</artifactId>
          <version>3.2.6.ga</version>
      </dependency>

Jeśli potencjalnie chcemy używać adnotacji do kontrolowania Hibernate, wówczas trzeba dodać jeszcze taką oto zależność:

 <dependency>
    	<groupId>org.hibernate</groupId>
    	<artifactId>hibernate-annotations</artifactId>
    	<version>3.4.0.GA</version>
    </dependency>

Poprzez dodanie tych dwóch zależności do listy używanych bibliotek zostanie przy okazji dołączony szereg innych. Teraz dla odmiany z konsoli uruchomimy Mavena by pociągnął wszystkie świeże zależności. Polecenie jest proste:

mvn compile

Niestety okazało się, że nie udało się dociągnąć zależności JTA – zrobimy to ręcznie – wchodząc na podaną przez Mavena stronę i ściągając plik JAR na nasz komputer. Potem w konsoli:

mvn install:install-file -DgroupId=javax.transaction -DartifactId=jta -Dversion=1.0.1B -Dpackaging=jar -Dfile=sciezka\do\pliku\jar\wraz\z\nim\samym

No i udało się – plik zainstalowany. Jednakże samo Hibernate to także plik konfiguracyjny. No i tutaj trzeba utworzyć ten plik, a także wskazać go aplikacji. W tym celu w katalogu projektu (konkretnie ścieżka to src/main/resources) tworzymy katalog hibernate i tam wrzucimy nasz plik hibernate.cfg.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>

    <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
    <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
    <property name="hibernate.connection.url">jdbc:postgresql://88.198.31.169/chlebik</property>
    <property name="hibernate.connection.username">moj_tajny_login</property>
    <property name="hibernate.connection.password">moje_tajne_haslo</property>


  </session-factory>
</hibernate-configuration>

I teraz w pliku POM.xml w elemencie build dodajemy taki wpis:

 <resources>
      <resource>
        <directory>src/main/resources/hibernate</directory>
      </resource>
 </resources>

Na sam koniec zaś musimy ściągnąć nasz connector do PostgreSQL. I znów kolejna zależność.

   <dependency>
       <groupId>postgresql</groupId>
       <artifactId>postgresql</artifactId>
       <version>8.3-603.jdbc4</version>
   </dependency>

Ufff, już teraz wiem co to znaczy konfiguracja XMLem. Pozostaje mieć nadzieję, że to wszystko jakoś działa, lub też będzie działać. Nie chciałbym wychodzić za bardzo do przodu i implementować tutaj DAO by sprawdzić czy w ogóle to połączenie działa (mam cichą nadzieję, że tak). Na razie zatem temat zostawiam – kiedy dojdziemy do implementowania logiki biznesowej aplikacji wówczas z całą pewnością zmienię treść wpisu gdyby cosik nie działało jak powinno.

Na sam koniec jeszcze dodatek do konfiguracji Mavena. Otóż podczas budowania projektów dostawałem wciąż komunikat o tym, iż kodowanie projektu to CP-1250 i w związku z tym aplikacja nie będzie przenośna (też nowość). Próby zmiany tego stanu rzeczy w właściwościach projektu nie dawały efektu, dlatego też trzeba było znów odwiedzić plik POM.xml. W sekcji build dorzucamy takie coś:

 <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>2.4.2</version>
        <configuration>
          <encoding>UTF-8</encoding>
        </configuration>
 </plugin>

I w ten oto sposób po zbudowaniu projektu (no i rzecz jasna ściągnięciu plugina) nasz output radośnie będzie generowany w UTF-8. Jak to w cywilizowanych krajach się dzieje 🙂 To tyle na dziś – w następnym wpisie spróbujemy coś zrobić z widokiem i postawić jakieś bardziej rozbudowane “Hello World”.

Ostateczne dodanie wpisu w ProgramBash

Umilkło ostatnio na blogu, dopadła mnie mądrze pisząc “prokrastynacja”, a po polsku – nie chciało mi się jak cholera.

Jednakże poczucie obowiązku robi swoje – dziś ostatni odcinek w cyklu “dodajemy wpisy w ProgramBash“. Zasadniczo nie za wiele tutaj nowości – widok, walidacja, zapis do bazy. Pozwoliłem sobie nie przeklejać listingów niezbyt różniących się od tych z poprzedniego wpisu dotyczącego ProgramBash. Jedyne co wypada pokazać, to użycie kolejnego komponentu Richafaces, jakim jest komponent DataScroller. Jest to nie mniej i nie więcej, a paginator dla danych, które zamierzamy wyświetlić. Oto kod:

 <h:form id="topForm">

        <rich:datascroller align="left" for="topList" maxPages="20"
             reRender="sc2, topList" id="sc1" oncomplete="SyntaxHighlighter.highlight()" />
     
        <rich:dataTable width="755" id="topList" rows="2" columnClasses="col"
           value="#{entryBean.topList}" var="entry"> 
            
            <f:facet name="header">
                <rich:columnGroup>
                    <h:column>        
                    </h:column>
                </rich:columnGroup>
            </f:facet>

            <h:column>
                Punktów: <h:outputText value="#{entry.points}" /><br />
                Dodał: <h:outputText value="#{entry.author.nick}" /><br />
                W kategorii <strong><h:outputText value="#{entry.category}" /></strong> w dniu: <strong><h:outputText value="#{entry.adddate}" /></strong><br /><br />

                <pre class="brush: <h:outputText value='#{entry.category.shortname}' />">
                    <h:outputFormat value="#{entry.entry}" />
                </pre>


            </h:column>


        </rich:dataTable>
        <rich:datascroller align="left" for="topList" maxPages="20"
          id="sc2" reRender="sc1, topList"  oncomplete="SyntaxHighlighter.highlight()">
        </rich:datascroller>

      
    </h:form>

Umieszczony w odpowiednim widoku komponent ten zaczytuje ze stosownego beana ( entryBean ) wszystkie wpisy posortowane po ilości posiadanych punktów. Jest to o tyle hardkorowe, że w przypadku np. 100k wpisów mielibyśmy spore narzuty na wydajności bazy i całej aplikacji. Jednakże w środowisku produkcyjnym do takich rzeczy standardowo uzywałoby się jakiegoś cache. Na potrzeby edukacyjne nie ma problemu – możemy zostawić to tak jak jest.

Domyślnie zaś ilość wyświetlanych rekordów na stronę to 2 – nie jest to zbyt duża ilość, ale chodziło mi o pokazanie możliwości paginacji bez musu tworzenia testowych 30 wpisów. Na razie pozostanie to wszystko bez zmian. Należy także zwrócić uwagę na podpięcie formatowania kodu – służą do tego znaczniki pre wraz z odpowiednią klasą. Konkretna klasa jest reprezentowana przez nową składową encji EntryCategory – shortname. Uwaga! Należy zwrócić szczególną uwagę na atrybut oncomplete i wywołanie metody SyntaxHighlighter.highlight()! Bez tego po wgraniu nowej strony z kodem nie zostanie on sformatowany!!! Straciłem na to godzinę szukania i kombinowania.

Problem polega na tym, iż na moim lokalnym kompie rozwiązanie powyższe śmiga pięknie. Co więcej – niezależnie od tego, czy do dataScrollera wrzucę bezpośrednio atrybut page, czy też nie. Niezależnie również od tego czy trzymam tę wartość w beanie o zasięgu sesji, czy pojedynczego requestu. Wszystko jest OK. Zaś na VPSie – ZONK! Naciskając przyciski zmieniające stronę, podgląd odpowiedzi serwera wskazuje na to, iż aplikacja otrzymała AJAXem jak najbardziej kolejną stronę wyników. Niestety, nie jest to odzwierciedlane w oknie programu. Grzebię w tym już chyba od tygodnia i nic nie jestem w stanie na to poradzić mimo rozlicznych kombinacji. Jeśli ktoś miałby jakiś pomysł – będę wdzięczny.

Efekt końcowy wygląda mniej więcej tak:

Do tego niestety pomimo najszczerszych chęci przegrałem z SyntaxHighlighterem jeśli chodzi o formatowanie kodu, w którym znajduje się ENTER na końcu pierwszej linii. Dorzuca on sobie kilkanaście spacji na początku linii, co wygląda dziwnie (obrazek powyżej), ale co więcej – nie daje się usunąć choćby JSem!!! Prototype dostarczany razem z Richfaces wyrzuca mi błędami (np. jego funkcja remove oficjalnie nie istnieje), zaś próba podpięcia jQuery pod aplikację kończy się prawie śmiercią kliniczną Richfaces. Trudno, cóż począć.

To tyle na dziś – niewiele w sumie zostało rzeczy, które chciałbym do aplikacyjki dodać – na pewno ocenianie wpisów (AJAX), a także możliwość otrzymania danych poprzez webservice (to tak celem przećwiczenia takowych w Javie). Zatem do następnego razu.

PS. Nie lubię JSF.