11 marca 2007

Java Persistence - Rozdział 3.6 Query API

Ta relacja okazała się być inna niż inne. Rozpocząłem wdrażanie pomysłu, do którego przymierzałem się od dłuższego czasu - testowanie wiadomości jako testów TestNG. Po napisaniu artykułu na ten temat - Java Persistence API z OpenJPA i Derby oraz TestNG z Eclipse IDE w tle - byłoby nieroztropnie nie skorzystać z okazji i nie wdrożyć idei w życie. Mimo, że sama lektura zajęła chwilę, to już skonstruowanie przykładów już trwało dłużej. Ciekawym efektem konieczności stworzenia przykładu jest dokładniejsze zrozumienie działania danej funkcjonalności. Pojawiło się kilka pytań, kilka testów nie poszło za pierwszym razem i ostatecznie czuję, że zrozumienie Query API oceniam na dobre 3+. Ciekawe jak się przyjmie taka forma relacjonowania?

Interfejs publiczny klasy javax.persistence.Query (Query API) dostarcza dwa rodzaje zapytań - statyczne (nazwane - ang. static/named queries) oraz dynamiczne (ang. dynamic queries). Zapytaniem statycznym nazywamy zapytanie zdefiniowane w adnotacji za pomocą @NamedQuery, a zapytaniem dynamicznym jest zapytanie, które jest zdefinowane bezpośrednio w kodzie aplikacji. Query API dostarcza również możliwość wiązania parametrów (przekazywanie wartości do miejsc w zapytaniu oznaczonych jako zmienne) oraz stronicowanie (zawężanie wyników z bazy danych - dzielenie ich logicznie na strony).

Query API składa się z metod, które kontrolują wykonanie zapytań SQL (SELECT, UPDATE lub DELETE).

List getResultList() - wykonuje zapytanie SELECT i zwraca wynik jako java.util.List.

Query query = em.createQuery("SELECT p FROM Pracownik p");
List<Pracownik> pracownicy = query.getResultList();

Object getSingleResult() - wykonuje zapytanie SELECT, które zwraca pojedyńczy wynik.

Query query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie AND p.nazwisko = :nazwisko");
query.setParameter("imie", "Jacek");
query.setParameter("nazwisko", "Laskowski");
Pracownik pracownik = (Pracownik) query.getSingleResult();

int executeUpdate() - wykonuje zapytanie UPDATE lub DELETE zwracając ilość zmodyfikowanych lub skasowanych wierszy. Metoda pozwala na wykonanie większej ilości modyfikacji danych w bazie danych bez tworzenia encji.

EntityTransaction tx = em.getTransaction();
tx.begin();
Query query = em
.createQuery("UPDATE Pracownik p SET p.nazwisko = :noweNazwisko WHERE p.imie = :imie AND p.nazwisko = :nazwisko");
query.setParameter("imie", "Agata");
query.setParameter("nazwisko", "Bretes");
query.setParameter("noweNazwisko", "Laskowska");
int iloscUaktualnionychRekordow = query.executeUpdate();
tx.commit();

EntityTransaction tx = em.getTransaction();
tx.begin();
Query query = em.createQuery("DELETE FROM Pracownik p");
int iloscUsunietychPracownikow = query.executeUpdate();
tx.commit();

Query setMaxResults(int maxResult) - ustawia maksymalną ilość wyników przy wykonaniu zapytania

query = em.createQuery("SELECT p FROM Pracownik p");
iloscPracownikow = query.getResultList().size();
assert iloscPracownikow > 3;
query = em.createQuery("SELECT p FROM Pracownik p");
query.setMaxResults(2);
iloscPracownikow = query.getResultList().size();
assert iloscPracownikow == 2;

Query setFirstResult(int startPosition) - ustanawia numer pierwszego wyniku w zbiorze wszystkich wyników (rozpoczynamy od 0)

query = em.createQuery("SELECT p FROM Pracownik p");
iloscPracownikow = query.getResultList().size();
assert iloscPracownikow == 4;
query = em.createQuery("SELECT p FROM Pracownik p");
query.setFirstResult(3);
iloscPracownikow = query.getResultList().size();
assert iloscPracownikow == 1;

Query setHint(String hintName, Object value) - przypisuje wskazówkę do zapytania specyficzną dla wykorzystywanego dostawcy JPA. Nieznane dla dostawcy JPA wskazówki są ignorowane.

query = em.createQuery("SELECT p FROM Pracownik p");
query.setHint("openjpa.hint.OracleSelectHint", "/*+ first_rows(100) */");
query.setHint("org.hibernate.readOnly", "true");
query.setHint("toplink.pessimistic-lock", "Lock");
iloscPracownikow = query.getResultList().size();

Query setParameter(String name, Object value) - ustawia wartość nazwanego parametru w zapytaniu. Należy zwrócić uwagę na nazwę parametru oraz jego typ.

query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter("imie", "Jacek");
iloscPracownikow = query.getResultList().size();

Query setParameter(String name, Date value, TemporalType temporalType) - przypisuje egzemplarz java.util.Date do nazwanego parametru w zapytaniu. Należy zwrócić uwagę na nazwę parametru oraz jego typ.

Calendar kalendarz = Calendar.getInstance();
kalendarz.set(Calendar.HOUR_OF_DAY, 0);
kalendarz.set(Calendar.MINUTE, 0);
kalendarz.set(Calendar.SECOND, 0);
kalendarz.set(Calendar.MILLISECOND, 0);
Date dzisiaj = kalendarz.getTime();
query = em.createQuery("SELECT p FROM Pracownik p WHERE p.dzienUrodzin = :dzisiaj");
query.setParameter("dzisiaj", dzisiaj, TemporalType.DATE);
List<Pracownik> pracownicy = query.getResultList();

Query setParameter(String name, Calendar value, TemporalType temporalType) - przypisuje egzemplarz java.util.Calendar do nazwanego parametru w zapytaniu. Należy zwrócić uwagę na nazwę parametru oraz jego typ.

Calendar kalendarz = Calendar.getInstance();
kalendarz.set(Calendar.HOUR_OF_DAY, 0);
kalendarz.set(Calendar.MINUTE, 0);
kalendarz.set(Calendar.SECOND, 0);
kalendarz.set(Calendar.MILLISECOND, 0);
query = em.createQuery("SELECT p FROM Pracownik p WHERE p.dzienImienin = :dzisiaj");
query.setParameter("dzisiaj", kalendarz, TemporalType.DATE);
List<Pracownik> pracownicy = query.getResultList();

Query setParameter(int position, Object value) - ustawia wartość parametru pozycyjnego w zapytaniu. Należy zwrócić uwagę na nazwę i typ parametru.

query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter(1, "Jacek");
List<Pracownik> pracownicy = query.getResultList();

Query setParameter(int position, Date value, TemporalType temporalType) - przypisuje egzemplarz java.util.Date do parametru pozycyjnego w zapytaniu. Należy zwrócić uwagę na nazwę i typ parametru.

Calendar kalendarz = Calendar.getInstance();
kalendarz.set(Calendar.HOUR_OF_DAY, 0);
kalendarz.set(Calendar.MINUTE, 0);
kalendarz.set(Calendar.SECOND, 0);
kalendarz.set(Calendar.MILLISECOND, 0);
Date dzisiaj = kalendarz.getTime();

query = em.createQuery("SELECT p FROM Pracownik p WHERE p.dzienUrodzin = :dzienUrodzin");
query.setParameter(1, dzisiaj);
List<Pracownik> pracownicy = query.getResultList();

Query setParameter(int position, Calendar value, TemporalType temporalType) - przypisuje egzemplarz java.util.Calendar do parametru pozycyjnego w zapytaniu. Należy zwrócić uwagę na nazwę i typ parametru.

Calendar kalendarz = Calendar.getInstance();
kalendarz.set(Calendar.HOUR_OF_DAY, 0);
kalendarz.set(Calendar.MINUTE, 0);
kalendarz.set(Calendar.SECOND, 0);
kalendarz.set(Calendar.MILLISECOND, 0);

query = em.createQuery("SELECT p FROM Pracownik p WHERE p.dzienImienin = :dzisiaj");
query.setParameter(1, kalendarz);
List<Pracownik> pracownicy = query.getResultList();

Query setFlushMode(FlushModeType flushMode) - ustawia tryb synchronizacji używany podczas wykonania zapytania, który nadpisuje ustawienia zarządcy trwałości.

Wszystkie metody zwracające Query pozwalają ustanowić ciąg wywołań w postaci jednego wywołania, np.

em.createQuery("SELECT p FROM Pracownik p")
.setFirstResult(10)
.setMaxResults(10)
.setHint("org.hibernate.readOnly", "true")
.getResultList()

Elementy wyniku zapytania, którego klauzula SELECT składa się z wielu wyrażeń kolumnowych jest typu Object[].

List<Object[]> pracownicyJakoTablica = em.createQuery("SELECT p.numer, p.imie, p.nazwisko FROM Pracownik p")
.getResultList();
assert pracownicyJakoTablica.get(0).length == 3;

W szczególności, przy klauzuli SELECT z jednym wyrażeniem kolumnowym typem jest Object.

List<Object> pracownicyJakoTablica = em.createQuery("SELECT p.numer FROM Pracownik p").getResultList();
assert pracownicyJakoTablica.get(0) instanceof Long;

Użycie natywnych zapytań SQL (utworzenie Query poprzez metody createNativeQuery), mapowanie wyników (opisane poniżej), wyznacza ilość elementów w tablicy Object[].

List<Object> pracownicyJakoTablica = em.createNativeQuery("SELECT numer FROM Pracownik").getResultList();
assert pracownicyJakoTablica.get(0) instanceof Long;

lub

List<Object[]> pracownicyJakoTablica = em.createNativeQuery("SELECT numer, imie, nazwisko FROM Pracownik")
.getResultList();
assert pracownicyJakoTablica.get(0).length == 3;

Rezultat wykonania setMaxResults oraz setFirstResults dla zapytań korzystających z FETCH JOIN po kolekcjach jest niezdefiniowany (gdybym to ja wiedział, co to są FETCH JOIN!)

Metody Query, poza executeUpdate, nie wymagają aktywnego kontekstu transakcyjnego. W szczególności, metody getResultList i getSingleResult nie wymagają aktywnej transakcji. Jeśli zarządca encyjny (trwałości) jest związany z transakcją (ang. transaction-scoped), tj. rozpoczęcie (begin) oraz zatwierdzenie (commit) czy wycofanie (rollback) powodują synchronizację stanu encji, wynikowe encje będą odłączone (ang. detached). W przypadku zarządcy encyjnego o rozszerzonym kontekście transakcyjnym (ang. extended persistence context), zwrócone encje będą trwałe. Więcej dywagacji na ten temat znajduje się w rozdziale 5 (który mnie z niecierpliwością oczekuje. Tym samym przykładów nie będzie, nie dałbym rady - zresztą jak sprawdzić, czy encja jest odłączona?).

Wyjątki niekontrolowane/uruchomieniowe (ang. unchecked/runtime exceptions) rzucane przez metody interfejsu Query, poza NoResultException czy NonUniqueResultException (oba należą do pakietu javax.persistence), powodują wycofanie transakcji.

EntityTransaction tx = em.getTransaction();
tx.begin();
Query query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter("imie", "Jacek");
Pracownik p = (Pracownik) query.getSingleResult();
assert p != null;
query.setParameter("imie", "nieistnieje");
try {
query.getSingleResult();
assert 0 == 1 : "Nie powinno nas tu być! Oczekiwano NoResultException!";
} catch (NoResultException nre) {
// oczekiwano, sprawdźmy stan transakcji
assert tx.getRollbackOnly() == false : "Przechwycono wyjątek NoResultException, więc transakcja powinna być wciąż aktywna.";
tx.commit();
}
assert tx.isActive() == false;

ale

Query query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter("imie", "Jacek");
Pracownik p = (Pracownik) query.getSingleResult();
assert p != null;

// Do wyjaśnienia na grupie Apache OpenJPA
em.clear();

EntityTransaction tx = em.getTransaction();
tx.begin();
assert tx.isActive() == true;
try {
em.persist(p);
// tutaj niekoniecznie rzucony wyjątek - mamy dwa wyjścia - em.flush, albo tx.commit
// Obojętnie, kto jest dostawcą JPA tx.commit() na prewno zrobi swoje
tx.commit();
assert 0 == 1 : "Nie powinno nas tu być! Oczekiwano EntityExistsException!";
} catch (EntityExistsException eee) {
// oczekiwano, sprawdźmy stan transakcji
assert tx.getRollbackOnly() == true : "Przechwycono wyjątek EntityExistsException, więc transakcja wycofywana.";
tx.rollback();
}
assert tx.isActive() == false;

Kolejny podrozdział 3.6.2 opisuje tryby synchronizacji przypisane do zapytań (ang. flush mode), które jak pamiętam były już gdzieś w specyfikacji JPA opisane. Mimo to, dobrze sobie przypomnieć je i tym razem.

Z działaniem zapytania związany jest tryb synchronizacji. Specyfikacja JPA dostarcza 2 tryby synchronizacji (klasa wyliczeniowa javax.persistence.FlushModeType):
  • COMMIT
  • AUTO (domyślne ustawienie)
Ich efekt zależy od stanu transakcji, w ramach której działa zapytanie.

Jeśli zapytania wykonywane są w ramach aktywnej transakcji i tryb opróźniania AUTO jest w użyciu (albo na zarządcy trwałości bądź bezpośrednio na egzemplarzu Query), wtedy dostawca trwałości zobowiązany jest zapewnić, że wszystkie modyfikacje stanu encji w PU, które mogłyby wpłynąć na wykonanie zapytania są widoczne dla niego. Dostawca trwałości ma prawo spełnić to wymaganie różnymi sposobami, wliczając zapisanie stanu encji do bazy danych. Jeśli ustawiono tryb opróżniania COMMIT, wynik działania zapytania jest nieokreślony.

EntityTransaction tx = em.getTransaction();
tx.begin();
assert tx.isActive() == true;
Query query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter("imie", "Jacek");
Pracownik p = (Pracownik) query.getSingleResult();
assert p != null;

query = em.createQuery("UPDATE Pracownik p SET p.imie = :noweImie WHERE p.imie = :imie");
query.setParameter("imie", "Jacek");
query.setParameter("noweImie", "InnyJacek");
int uaktualniono = query.executeUpdate();

assert uaktualniono == 1;

assert em.getFlushMode() == FlushModeType.AUTO;

query = em.createQuery("SELECT p FROM Pracownik p WHERE p.imie = :imie");
query.setParameter("imie", "InnyJacek");
p = (Pracownik) query.getSingleResult();
assert p != null;

tx.commit();
assert tx.isActive() == false;

Jeśli zapytania wykonywane są poza transakcją, niedozwolone jest, aby dostawca trwałości synchronizował zawartość bazy ze stanem encji, tj. zapisywał bieżące zmiany stanu encji bez wyraźnego polecenia - wywołania metody flush.

3.6.3 Nazwane parametry

Nazwanym parametrem nazywamy identyfikator zapytania poprzedzony znakiem ":" (dwukropek). Parametry nazwane są wrażliwe na wielkość liter.

Nazwane parametry muszą przestrzegać reguł nazewnictwa opisanych w sekcji 4.4.1 (czyli nie wiem, bo wciąż ten rozdział przede mną). Użycie nazwanych parametrów jest możliwe wyłącznie dla zapytań korzystających z JPA-QL i nie jest określony dla zapytań natywnych SQL. Jedynie parametry pozycyjne gwarantowane są działać z zapytaniami natywnymi SQL (a to bardzo ciekawe, gdyż w Java EE Tutorial w sekcji Named Parameters in Queues rozdziału The EntityManager napisano, że parametry nazwane mogą być używane przez zapytania nazwane i statyczne - planuję to wyjaśnić na forach EJB/JPA).

Nazwy parametrów nazwanych przekazywanych do metod Query.setParameters nie zawierają przedrostka ':'.

Query query = em
.createQuery("UPDATE Pracownik p SET p.nazwisko = :noweNazwisko WHERE p.imie = :imie AND p.nazwisko = :nazwisko");
query.setParameter("imie", "Agata");
query.setParameter("nazwisko", "Bretes");
query.setParameter("noweNazwisko", "Laskowska");

3.6.4 Nazwane zapytania

Nazwane zapytania są statycznymi zapytaniami zdefiniowanymi poprzez metadane - adnotacje lub w deskryptorze XML. Nazwane zapytania mogą być wyrażone w JPA-QL bądź SQL. Nazwy zapytań nazwanych są związane z PU.

Definicja nazwanego zapytania korzystając z JPA-QL w klasie encji:

@NamedQuery(name = "znajdzPracownikowPoImieniu", query = "SELECT p FROM Pracownik p WHERE p.imie LIKE :imie")

i jego użycie:

Query query = em.createNamedQuery("znajdzPracownikowPoImieniu");
query.setParameter("imie", "Jacek");
Pracownik p = (Pracownik) query.getSingleResult();
assert p != null;

oraz dla przypadku zapytań natywnych w SQL:

@NamedNativeQuery(name = "znajdzPracownikowPoImieniuSQL", query = "SELECT * FROM Pracownik WHERE imie LIKE ?")

i jego użycie:

Query query = em.createNamedQuery("znajdzPracownikowPoImieniuSQL");
query.setParameter(1, "Jacek");
Object[] polaPracownika = (Object[]) query.getSingleResult();
assert polaPracownika.length > 0;

3.6.5 Zapytania polimorficzne

Domyślnie wszystkie zapytania są polimorficzne, tj. klauzula FROM zapytania wyznacza wyłącznie klasę nadrzędną, której dotyczy zapytanie, ale wszystkie klasy podrzędne (a w zasadzie encje reprezentowane przez te klasy) również w nim uczestniczą. Rozważa się umożliwienie zawężenia polimorfizmu zapytania w przyszłych wydaniach specyfikacji.

EntityTransaction tx = em.getTransaction();
tx.begin();
em.createQuery("DELETE FROM Pracownik p").executeUpdate();
Pracownik jacekLaskowski = new Pracownik("Jacek", "Laskowski");
em.persist(jacekLaskowski);

Query query = em.createNamedQuery("wszyscyPracownicy");
List<Pracownik> pracownicy = query.getResultList();
assert pracownicy.size() == 1;
assert pracownicy.get(0) instanceof Pracownik;

assert em.getFlushMode() == FlushModeType.AUTO;

Pracownik prezes = new PracownikSpecjalny("Jan", "Kowalski", "Prezes");
em.persist(prezes);
pracownicy = query.getResultList();
assert pracownicy.size() == 2;
tx.rollback();

3.6.6 Zapytania natywne SQL

Zapytania mogą być wyrażone w języku SQL wraz z elementami specyficznymi dla języka SQL w wykorzystywanej bazie danych. Wynik zapytania może składać się z wartości skalarnych, encji i ich kombinacji. Zwrócone z zapytania encje mogą być różnych typów.

UWAGA: Korzystanie z natywnych zapytań powinno być minimalne, gdzie JPA-QL nie jest wystarczająco ekspresyjny. Możliwość korzystania z elementów języka SQL specyficznego dla wykorzystywanej bazy danych powoduje, że zapytanie, a tym samym i aplikacja, jest nieprzenośna względem baz danych, tj. związana jest z konkretną bazą danych.

W przypadku zapytania natywnego SQL, które zwraca wiele encji, encje muszą być określone i związane z odpowiednimi kolumnami z zapytania SQL poprzez adnotację @SqlResultSetMapping. Mapowanie będzie wykorzystane przez dostawcę JPA do przypisania wyniku do odpowiednich klas encji.

Ciekawy przykład znajduje się w dokumentacji @SqlResultSetMapping.

Jeśli wyniki zapytania są ograniczone do encji pojedyńczej klasy trwałej, można skorzystać z prostszego sposobu niekorzystającego z @SqlResultSetMapping.

Query query = em.createNativeQuery("SELECT numer, imie, nazwisko FROM Pracownik", Pracownik.class);
List<Pracownik> pracownicy = query.getResultList();

W przypadku, kiedy encja jest zwracana, zapytanie SQL skorzysta ze wszystkich kolumn, które są przypisane (zmapowane) do encji, wliczając w to kolumny kluczy obcych do odpowiednich encji. Wynik otrzymany przy niewystarczających danych jest nieokreślony. Wynik zapytania SQL nie może być mapowany do pól nietrwałych encji.

Nazwy kolumn w adnotacjach mapujących wynik zapytania SQL wskazują na nazwy kolumn w klauzuli SELECT zapytania. Wymaga się skorzystania z aliasów w klauzuli SELECT zapytania, kiedy więcej niż jedna kolumna w wyniku zapytania nosi tę samą nazwę.

Typy skalarne mogą być zawarte w zapytaniu przez @ColumnResult.

W przypadku, kiedy encja jest właścicielem relacji jednowartościowej (ang. single-valued relationship) i klucz obcy jest złożony (składający się z kilku kolumn), element FieldResult powinien być użyty dla każdej kolumny klucza obcego. Element FieldResult musi korzystać z notacji z kropką '.' do wyznaczenia mapowania kolumn do właściwości/pola klucza głównego docelowej encji.

Nie wymaga się, aby notacja mapująca z kropką była wspierana dla innego mapowania niż dla złożonych kluczy obcych lub wbudowanych kluczy głównych.

Jeśli docelowa encja ma klucz główny typu @IdClass, określenie składa się z nazwy pola/właściwości relacji, po której następuje '.' (kropka), po której następuje pole/właściwość klucza głównego w docelowej encji (oznaczonej adnotacją @Id).

Elementy FieldResult dla złożonego klucza obcego tworzą klasę EmbeddedId klucza głównego dla docelowej encji. Może to być wykorzystane do pozyskania encji jeśli relacja jest ustawiona jako wcześnie materializowana.

Użycie nazwanych parametrów nie jest zdefiniowane dla zapytań natywnych SQL. Jedynie przypisywanie pozycyjnych parametrów jest wspierane.

Wsparcie dla JOIN jest ograniczone do relacji jednowartościowych.

Wiele ciekawych przykładów dotyczących zapytań natywnych SQL i ich mapowania jest zamieszczonych w specyfikacji, do lektury której gorąco zachęcam.