13 kwietnia 2007

Java Persistence - Zakończenie rozdziału 4. Język zapytań - rozdziały 4.9 - 4.14

Kto by uwierzył?! Ja na pewno nie! I w dodatku - w piątek 13-tego! Ten wpis jest 100-nym wpisem w historii Notatnika. Skoro stuknęła 100-tka to i temat musi być ciekawy - padło na...dokończenie rozdziału 4 Język zapytań Java Persistence. Bo jak mogłoby być inaczej?! ;-) W końcu może doczekam się ukończenia specyfikacji i przejścia do kolejnej części o Enterprise JavaBeans 3.0 - EJB Core Contracts and Requirements. Tyle z imprezy z okazji 100-nego wpisu. Niedługo to trwało.

Zanim rozpocznę relację kończącą rozdział 4 Język zapytań wspomnę o pojawieniu się nowej wersji TopLink Essentials 2.0 BUILD 41 zwanego również TopLink JPA. Uaktualnienie produktu w projekcie zarządzanym przez Apache Maven (jak opisałem w artykule Nauka Java Persistence z Apache Maven 2 i dostawcami JPA: OpenJPA, Hibernate i TopLink) sprowadza się do podniesienia numeru wersji zależności w pom.xml (wersja 2.0-41 znajduje się w repozytorium M2). Zmiany nie są imponujące, ale będąc przekonanym, że w przypadku projektów otwartych jedynie błędy w najnowszych wersjach mają szansę na poprawkę oraz mojemu zaangażowaniu we wsparciu projektów w ich testowaniu nie zastanawiam się wiele nad podjęciem decyzji. Podnoszę wersję do 2.0 BUILD 41!

Poza tym pojawiła się również nowa wersja Hibernate Core 3.2.3. Podobnie jak z TopLink JPA nie zauważyłem znaczących poprawek, które miałyby wpłynąć na moje poznawanie specyfikacji JPA, ale mimo wszystko podnoszę również i wersję Hibernate. Tym razem podniesienie wersji sprowadza się do zmiany wersji w pom.xml oraz instalacji projektu do lokalnego repozytorium M2, gdyż wersja 3.2.3 nie jest jeszcze dostępna w repozytorium M2.

mvn install:install-file -DgroupId=org.hibernate -DartifactId=hibernate \
-Dversion=3.2.3.ga -Dpackaging=jar -Dfile=c:/apps/hibernate-3.2.3.GA/hibernate3.jar

Poza tym konieczne jest jeszcze przekopiowanie pliku hibernate-3.2.3.ga.pom z poprzedniej wersji Hibernate wraz ze zmianą wersji wewnątrz, gdyż w przeciwnym wypadku konieczne byłoby samodzielne zarządzanie zależnościami Hibernate (nauka żadna a roboty dużo - niepotrzebnie).

Swojego uaktualnienia doczekał również projekt Apache OpenJPA do wersji 0.9.8-incubating-SNAPSHOT, jednakże ostateczna decyzja o opublikowaniu wersji stabilnej 0.9.7-incubating jest jeszcze głosowana. Mimo wszystko źródła projektu już oznaczone zostały jako wersja 0.9.8-incubating-SNAPSHOT.

Po uaktualnieniu pom.xml, krótki test, czy działają uaktualnienia...

[TopLink Info]: 2007.04.12 11:23:40.796--ServerSession(4667711)--Thread(Thread[main,5,main])--
TopLink, version: Oracle TopLink Essentials - 2.0 (Build 41 (03/30/2007))

oraz

23:22:50,046 INFO Environment:509 - Hibernate 3.2.3

i na koniec OpenJPA

0 derbyPU INFO [main] openjpa.Runtime - Starting OpenJPA 0.9.8-incubating-SNAPSHOT

i wracam do rozdziału 4. specyfikacji Java Persistence.

4.9 Klauzula ORDER BY

Klauzula ORDER BY pozwala na uporządkowanie wyników zapytania.

Składnia klauzuli ORDER BY jest następująca:

klauzula_orderby ::= ORDER BY element_orderby {, element_orderby}*
element_orderby ::= wyrażenie_ścieżkowe_pola_stanu [ASC|DESC]

Przykład: Uszereguj wszystkie osoby w porządku malejącym po dacie imienin.

SELECT o FROM Osoba o ORDER BY o.dzienImienin ASC

Kiedy ORDER BY jest używane, każdy element w klauzuli SELECT musi być jednym z następujących wyrażeń:
  • zmienna identyfikująca o, opcjonalnie zapisana z OBJECT(o)
  • wyrażenie ścieżkowe łączenia o pojedyńczej wartości
  • wyrażenie ścieżkowe pola stanu
W pierwszych dwóch przypadkach, każdy element_orderby musi być polem-stanu AST encji względem, którego można wykonać operację sortowania i który będzie wyznaczony w klauzuli SELECT (innymi słowy: wyznaczona encja, tj. AST encji, w klauzuli SELECT określa możliwy zbiór pól, po których można porządkować wyniki zapytania).

Przykład: Uszereguj rosnąco projekty otwarte po nazwie, do których przypisane są osoby (zakłada się, że nie istnieje relacja zwrotna Projekt->Osoba).

SELECT p FROM Osoba o JOIN o.projekty p WHERE p.rodzajProjektu = RodzajProjektu.OTWARTY ORDER BY p.nazwa DESC

Niemożliwe byłoby skorzystanie z innych pól trwałych niż należących do encji Projekt w klauzuli ORDER BY, gdyż jedynie zmienna identyfikująca p o typie Projekt występuje w klauzuli SELECT.

W trzecim przypadku, element_orderby wyznacza to samo pole-stanu AST encji, która użyta jest w klauzuli SELECT.

Przykład: Uszereguj rosnąco nazwy projektów otwartych, do których przypisane są osoby.

SELECT p.nazwa FROM Osoba o JOIN o.projekty p WHERE p.rodzajProjektu = RodzajProjektu.OTWARTY ORDER BY p.nazwa DESC

Niemożliwe byłoby skorzystanie z innego pola trwałego encji Projekt niż nazwa w klauzuli ORDER BY, gdyż nie jest ona zdefiniowana w klauzuli SELECT (występuje pojedyńcze wyrażenie ścieżkowe pola stanu).

Jeśli podano więcej element_orderby, kolejność ich występowania od lewej do prawej wyznacza ich ważność, gdzie najbardziej lewe kryterium sortowania ma najwyższy priorytet.

Słowo kluczowe ASC określa porządek rosnący podczas, gdy DESC porządek malejący. Domyślnie wyniki są sortowane rosnąco (ASC).

Zasada sortowania wartościami NULL określa jedynie, że wszystkie wartości NULL muszą wystąpić przed wartościami nie-NULL, lub na odwrót, ale nie określono jaki porządek jest domyślny.

4.10 Zbiorcze operacje modyfikacji (UPDATE) i usuwania (DELETE)

Zbiorcze operacje modyfikacji UPDATE i usuwania DELETE działają na encjach będących egzemplarzami tego samego typu bądź typów pochodnych. Jedynie pojedyńczy AST encji może być określony w klauzulach FROM lub UPDATE.

Składnia operacji UPDATE jest następująca:

wyrażenie_update ::= klauzula_update [klauzula_where]
klauzula_update ::= UPDATE nazwa_abstrakcyjnego_schematu [[AS] zmienna_identyfikująca] SET element_update {, element_update}*
element_update ::= [zmienna_identyfikacyjna.]{pole_stanu|pole_łączenia_o_pojedyńczej_wartości} = nowa_wartość
nowa_wartość ::=
proste_wyrażenie_arytmetyczne |
literał_łańcuchowy |
literał_kalendarzowy |
literał_logiczny |
literał_wyliczeniowy |
proste_wyrażenie_encyjne |
NULL

Składnia operacji DELETE jest następująca:

wyrażenie_delete ::= klazula_delete [klauzula_where]
klauzula_delete ::= DELETE FROM nazwa_anstrakcyjnego_schematu [[AS] zmienna_identyfikacyjna]

Składnia klauzuli WHERE (oznaczonej w tekście jako klauzula_where) wyjaśniona została w mojej poprzedniej relacji Java Persistence - Rozdziały 4.5 Klauzula WHERE oraz 4.6 Wyrażenia warunkowe (w specyfikacji jest to rozdział 4.5).

Operacja DELETE działa jedynie na encjach określonego typu i jego typów pochodnych. Operacja nie jest przechodnia na encje związane relacjami.

Przykład: Usunięcie osób należących do projektów komercyjnych

String countQL = "SELECT COUNT(o) FROM Osoba o JOIN o.projekty p WHERE p.rodzajProjektu = RodzajProjektu.KOMERCYJNY";
query = em.createQuery(countQL);
long iloscOsobWProjektachKomercyjnych = (Long) query.getSingleResult();
assert iloscOsobWProjektachKomercyjnych > 0 :
"Oczekiwano osób przypisanych do projektów komercyjnych, a otrzymano " + iloscOsobWProjektachKomercyjnych;

String deleteQL = "DELETE FROM Osoba o "
+ "WHERE (SELECT COUNT(p) FROM o.projekty p WHERE p.rodzajProjektu = RodzajProjektu.KOMERCYJNY) > 0";
em.getTransaction().begin();
query = em.createQuery(deleteQL);
int iloscUsunietychOsob = query.executeUpdate();
em.getTransaction().rollback(); // na potrzeby kolejnych testów
assert iloscUsunietychOsob == iloscOsobWProjektachKomercyjnych :
"Oczekiwano tej samej liczby osób usuniętych z liczbą osób przypisanych do projektów komercyjnych, a otrzymano usuniętych: "
+ iloscUsunietychOsob + ", przypisanych: " + iloscOsobWProjektachKomercyjnych;

Nie ukrywam, że jest to pierwsza tak złożona operacja DELETE i zapewne da się to zrobić prościej. Propozycje zmian mile widziane!

Kilka uwag odnośnie korzystania z operacji DELETE oraz UPDATE. Jak można zauważyć w powyższym przykładzie, operacje zbiorcze modyfikują stan bazy danych i wymagają aktywnej transakcji. Dodatkowo, wykonanie operacji zbiorczych wymaga skorzystania z metody Query.executeUpdate(), która zwraca liczbę zmodyfikowanych encji (niekoniecznie pojedyńczych rekordów, jeśli korzysta się ze złożonego mapowania na kilka tabel).

Wartość nowa_wartość dla operacji UPDATE musi być zgodna z typem pola trwałego, które jest modyfikowane.

Operacje zbiorcze wykonywane są bezpośrednio na bazie danych, omijając kontrolę optymistycznego blokowania. Aplikacje przenośne muszą samodzielnie uaktualnić i ewentualnie zweryfikować wartość kolumny wskazywanej przez atrybut wersjonowany (udekorowany adnotacją @Version).

Kontekst trwały nie jest odświeżany o wynik operacji zbiorczych, co oznacza, że po ich wykonaniu może pojawić się niespójność między stanem encji w kontekście i bazie danych. Zaleca się, aby operacje zbiorcze były wykonywane jedynie w osobnej transakcji lub na jej początku zanim encje zostaną użyte (załadowane do kontekstu trwałego), a których stan mógłby ulec modyfikacji jako wynik operacji zbiorczych.

Przykład: Niespójność między zarządcą pamięci a bazą danych po wykonaniu operacji UPDATE.

Query query;

final String imie = "Jacek";
final String noweImie = "JACEK";

EntityTransaction tx = em.getTransaction();
tx.begin();
try {
String countQL = "SELECT COUNT(o) FROM Osoba o WHERE o.imie = :imie";
query = em.createQuery(countQL);
query.setParameter("imie", imie);
long iloscOsob = (Long) query.getSingleResult();
assert iloscOsob > 0 : "Oczekiwano przynajmniej jednej osoby o imieniu Jacek, a otrzymano " + iloscOsob;

String selectQL = "SELECT o FROM Osoba o WHERE o.imie = :imie";
query = em.createQuery(selectQL);
query.setParameter("imie", imie);
Osoba jacek = (Osoba) query.getSingleResult();
assert imie.equals(jacek.getImie()) : "Oczekiwano Jacka, a otrzymano " + jacek;

String updateQL = "UPDATE Osoba o SET o.imie = :noweImie WHERE o.imie = :imie";
query = em.createQuery(updateQL);
query.setParameter("imie", imie);
query.setParameter("noweImie", noweImie);
int iloscZmodyfikowanychOsob = query.executeUpdate();
assert iloscZmodyfikowanychOsob == iloscOsob : "Oczekiwano wyłącznie zmiany Jacków, a zmieniono: "
+ iloscZmodyfikowanychOsob;

assert imie.equals(jacek.getImie()) : "Oczekiwano Jacka (zbiorczy UPDATE), a otrzymano " + jacek;

em.refresh(jacek); // konieczne po zbiorczym UPDATE

assert noweImie.equals(jacek.getImie()) : "Oczekiwano JACEK (po odświeżeniu), a otrzymano " + jacek;

} finally {
tx.rollback();
}

Ciekawostką było stworzenie przykładu, który dla Apache OpenJPA 0.9.7-SNAPSHOT nie stanowił przeszkody do poprawnego uruchomienia podczas, gdy Hibernate JPA oraz TopLink JPA zgłaszali błąd niespójności raportowany przez bazę danych. Niestety próba dojścia do przyczyny problemu z Hibernate JPA zakończyła się fiaskiem.

testUpdateInconsistency(pl.jaceklaskowski.jpa.Ch4_10Test) Time elapsed: 0.344 sec <<< FAILURE!
javax.persistence.EntityExistsException: org.hibernate.exception.ConstraintViolationException: could not execute update query
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:605)
at org.hibernate.ejb.QueryImpl.executeUpdate(QueryImpl.java:59)
at pl.jaceklaskowski.jpa.Ch4_10Test.testUpdateInconsistency(Ch4_10Test.java:63)

Szczęśliwie TopLink był niezwykle wylewny i przypomniał mi o zależności między encją Projekt a Osoba.

testUpdateInconsistency(pl.jaceklaskowski.jpa.Ch4_10Test) Time elapsed: 0.14 sec <<< FAILURE!
Local Exception Stack:
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build 41 (03/30/2007))):
oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: DELETE on table 'OSOBA' caused a violation of foreign key constraint 'PROJEKTCHAIR_NUMER' for key (2).
The statement has been rolled back.
Error Code: 20000
Call: DELETE FROM OSOBA
Query: DeleteAllQuery()
at oracle.toplink.essentials.exceptions.DatabaseException.sqlException(DatabaseException.java:296)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.executeDirectNoSelect(DatabaseAccessor.java:639)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.executeNoSelect(DatabaseAccessor.java:688)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:477)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:437)
at oracle.toplink.essentials.internal.sessions.AbstractSession.executeCall(AbstractSession.java:675)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:213)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.deleteAll(DatasourceCallQueryMechanism.java:102)
at oracle.toplink.essentials.queryframework.DeleteAllQuery.executeDatabaseQuery(DeleteAllQuery.java:183)
at oracle.toplink.essentials.queryframework.DatabaseQuery.execute(DatabaseQuery.java:609)
at oracle.toplink.essentials.queryframework.DatabaseQuery.executeInUnitOfWork(DatabaseQuery.java:536)
at oracle.toplink.essentials.queryframework.ModifyAllQuery.executeInUnitOfWork(ModifyAllQuery.java:153)
at oracle.toplink.essentials.queryframework.DeleteAllQuery.executeInUnitOfWork(DeleteAllQuery.java:109)
at oracle.toplink.essentials.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2219)
at oracle.toplink.essentials.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:937)
at oracle.toplink.essentials.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:909)
at oracle.toplink.essentials.internal.ejb.cmp3.base.EJBQueryImpl.executeUpdate(EJBQueryImpl.java:372)
at pl.jaceklaskowski.jpa.Ch4_10Test.testUpdateInconsistency(Ch4_10Test.java:63)

Zagadką pozostaje, dlaczego OpenJPA nie zgłosił błędu?!

Podczas tworzenia przykładu natrafiłem na jeszcze jedną ciekawostkę - błąd OpenJPA, który udało mi się zidentyfikować i zgłosić. Dla OpenJPA zmienne identyfikacyjne są obowiązkowe w klauzuli UPDATE podczas, gdy specyfikacja tego nie wymaga. Zgłosiłem go jako OPENJPA-216 ArgumentException when no identification variable given in UPDATE clause.

Przy okazji natrafiłem również na błąd w samej specyfikacji - ostatni przykład na stronie 105 rozdziału 4.10. Nazwa encji pisana jest z małej litery podczas, gdy powinna być zapisana z wielkiej. Poprawny przykład powinien wyglądać następująco:

UPDATE customer c
SET c.status = ‘outstanding’
WHERE c.balance < 10000
AND 1000 > (SELECT COUNT(o)
FROM customer cust JOIN cust.order o)


4.11 Wartości NULL

Jeśli wyrażenie w zapytaniu wskazuje na wartość nieistniejącą w bazie danych, jego wartością będzie NULL. Specyfikacja SQL-92 definiuje wyliczanie wyrażeń warunkowych zawierających NULL, co w skrócie można przedstawić jako:
  • Operacje porównania i arytmetyczne z udziałem NULL zwracają wartość nieokreśloną.
  • Dwie wartości NULL nie są identyczne i ich porównanie zwraca wartość nieokreśloną.
  • Operacje porównania i arytmetyczne z udziałem wartości nieokreślonej zwracają wartość nieokreśloną.
  • Operatory IS NULL i IS NOT NULL zamieniają wartość NULL pola-stanu lub pola-łączenia o pojedyńczej wartości na wartość logiczną - TRUE lub FALSE.
  • Operatory logiczne oparte są o logikę trójwartościową, gdzie wartościami są TRUE, FALSE oraz wartość nieokreślona.
Wynik działania operatorów logicznych przedstawiono w 3 tabelkach - dla operatora AND, OR oraz NOT (strona 106).

Specyfikacja zwraca uwagę na (ulotną) różnicę między pustym łańcuchem znaków o długości 0, a wartością NULL. Z punktu widzenia specyfikacji JPA, pusty łańcuch znaków nie jest równy NULL, jednakże należy zwrócić na to uwagę podczas wykonywania operacji porównania z niektórymi bazami danych (z oczywistych względów nie podano jakimi).

4.12 Semantyka równości i porównania

Jedynie wartości "podobnych" typów można porównywać. Dwa typy uważa się za "podobne", jeśli są tego samego typu w Javie, jeden z nich jest typu prostego podczas, gdy drugi jest typu opakowującego, tj. jest jego obiektowym odpowiednikiem (long vs Long i podobne).

Istnieje wyjątek od tej reguły, jeśli porównuje się wartości liczbowe, dla których działają reguły promocji. Wyrażenia warunkowe mogą jedynie porównywać typy "podobne" lub skorzystać z reguł promocji typów dla wartości liczbowych różnych typów.

Specyfikacja podkreśla, że operatory arytmetyczne i porównania są dozwolone dla wartości będących polami-stanu oraz parametrów wejściowych typu opakowującego (wysłałem zapytanie na forum EJB - 4.12 Equality and Comparison Semantics - asking for some explanation - w celu wyjaśnienia co autor miał na myśli).

Dwie encje tego samego AST są równe, wtedy i tylko wtedy, gdy wartości ich kluczy głównych są równe.

Jedynie porównania równości i nierówności nad typami wyliczeniowymi są wymagane. Pozostałe porównania są opcjonalne.

4.13 Przykłady

Rozdział przedstawia składnię i znaczenie (semantykę) zapytań JPQL. Warto się z nimi zapoznać dla utrwalenia tematu.

Na uwagę zasługuje przykład ze strony 108 dotyczący równości adresów - shippingAddress oraz billingAddress. W pierwszej części zakłada się, że oba adresy reprezentowane są przez różne encje podczas, gdy w drugim (udoskonalonym) opiera się o tą samą encję w różnych relacjach. Sprawdzenie równości adresów w przypadku drugim to porównanie ich samych (i skorzystanie z reguły równości encji na podstawie ich kluczy głównych z sekcji 4.12 opisanej wyżej).

4.14 BNF

Rozdział 4.14 kończy rozdział 4 i prezentuje całą notację BNF dla języka zapytań Java Persistence - JPQL. Całość obowiązkowo do zapamiętania na egzamin SCBCD 5 ;-)