25 kwietnia 2007

Teoretyczne wprowadzenie do GWT

0 komentarzy
Zanim podejdę do przedstawienia Google Web Toolkit (GWT), uczynię swoją powinność jak napisano na stronach Google:

Portions of this page are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 2.5 Attribution License.

Nie długo trwało zanim okazało się, że chciałem zacząć ten wpis tymi samymi słowami, jakimi rozpoczął swoje przedstawienie GWT Rafał Malinowski na swoim blogu. Natrafiłem na jego blog przeszukując materiały o GWT po polsku (nota bene, przypomniałem sobie, że o Rafale wspominał również Brzezi po moim pytaniu o wprowadzenie do GWT - Kilka pytań początkującego o GWT (było: Re: Spring + GWT) na grupie pl.comp.lang.java) .

Sercem GWT jest konwerter kodu w Javie (wersja 1.4.2 i poprzednie) na JavaScript.

I możnaby na tym poprzestać, gdyby nie fakt, że dla wielu (do niedawna również i dla mnie) JavaScript nie kojarzył się nierozerwalnie z technologią AJAX (mimo, że powinien, chociażby z samej nazwy AJAX - Asynchroniczny JavaScript i XML). I mimo, że GWT to konwerter Java do JavaScript to niekoniecznie musiało to implikować również zawarcie AJAXa. W przypadku GWT mamy do dyspozycji możliwość tworzenia kodu w Javie, który jest bardzo zbliżony do tworzenia aplikacji z wykorzystaniem biblioteki JFC/Swing, w tym i obsługą zdarzeń, która ostatecznie kończy swój "żywot rozwojowy" jako aplikacja napisana w JavaScript, właśnie ze wsparciem dla AJAX.

Dla mnie osobiście, ważne jest, że GWT pozwala zapomnieć o (X)HTML, DHTML, JavaScript i ostatecznie AJAX (czyli interakcją z XMLHttpRequest wraz z modyfikacją DOM) oraz o różnicach w realizacji ich w przeglądarkach - IE, FF, Safari i Opera (zobacz: Cross-browser Support).

Istotnym elementem jest również wykorzystanie dostępnych elementów HTML do konstrukcji aplikacji korzystającej z GWT. Strona HTML z kodem tworzonym przy pomocy GWT (ang. HTML host page) jest możliwa do uruchomienia bez specjalnych wtyczek/rozszerzeń po stronie klienta-przeglądarki, co czyni tworzenie aplikacji internetowej z wykorzystaniem GWT bezinwazyjnym i nie nakłada (prawie) żadnych wymagań, będąc wyłącznie "czystym" JavaScriptem.

Strona GWT (tymczasowa nazwa dla HTML host page) jest tworzona jako wynik transformacji kodu napisanego w Javie, gdzie mamy możliwość skorzystania z silnego typowania języka i skorzystać z dostępnych zintegrowanych środowisk programistycznych (ang. IDE - integrated development environment) wspierających tworzenie aplikacji w tym języku, do strony całkowicie pozbawionej elementów pisanych w Javie, a jedynie JavaScript.

Jak z każdą aplikacją HTML tak i w przypadku aplikacji GWT (która docelowo staje się aplikacją HTML z JavaScript) do ustawiania wizualizacji (stylu) kontrolek (ang. GWT widget) służy kolejny ustandaryzowany język - język styli CSS (zobacz: Style Sheets). Jest to kolejne wykorzystanie istniejącej technologii/języka, co czyni rozpoznanie i korzystanie z GWT prostszym.

Mamy dwa tryby pracy z GWT - tryb hosted (rozwojowy), w którym następuje uruchomienie testowe aplikacji przy pomocy specjalnie stworzonej dla GWT przeglądarki bez translacji kodu w Javie do kodu wynikowego w JavaScript oraz web (produkcyjny), gdzie klientem jest typowa przeglądarka (zobacz: Debugging in Hosted Mode). Większość naszego czasu nad rozwojem aplikacji GWT będziemy spędzali na pracy w trybie rozwojowym. Pozwala to na pełne wykorzystanie dostępnych narzędzi do tworzenia aplikacji Java. Podczas pracy w trybie rozwojowym, w przeglądarce GWT możemy skorzystać z przycisku Compile/Browse do wygenerowania odpowiedniego kodu w JavaScript bądź skorzystać ze skryptów tworzonych automatycznie podczas zestawiania projektu GWT.

Ważna podkreślenia jest ciągle powtarzana, ale jednocześnie trudna do zapamiętania, fundamentalna cecha GWT, która sprawia, że w trybie produkcyjnym aplikacja GWT to wyłącznie JavaScript i inne technologie klienckie (interpretowane przez przeglądarkę, jak CSS, DHTML, XML). Tym samym nie wymaga się żadnej wtyczki/rozszerzenia ani wirtualnej maszyny Java (JVM) do produkcyjnego uruchomienia aplikacji GWT.

GWT dostarcza skrypty tworzące projekt oraz skrypty uruchomieniowe dla narzędzi pomocniczych (zobacz: Command-line Tools):
  • projectCreator - służy do utworzenia struktury katalogowej projektu i opcjonalnych plików projektowych dla Ant i Eclipse.
  • applicationCreator - służy do utworzenia aplikacji, skryptów do uruchomienia jej w trybie rozwojowym oraz transformacji (kompilacji) do JavaScript.
  • junitCreator - służy do utworzenia testów jednostkowych JUnit i skryptów do uruchomienia w trybie rozwojowym i produkcyjnym (w zwykłej przeglądarce).
  • i18nCreator - usprawnia proces umiędzynarodowienia aplikacji (ang. i18n - internationalization), czyli dostosowywania aplikacji do wymagań specyficznych dla danego kraju.
W GWT mówi się o dwóch stronach aplikacji - stronie klienckiej (przeglądarka) oraz serwerowej (Client-side Code oraz Server-side Code). Mimo, że cała aplikacja GWT staje się ostatecznie wyłącznie zbiorem plików z JavaScript, HTML i CSS, i nie byłoby mowy o stronie innej niż przeglądarka, to w przypadku korzystania z AJAX (czyli GWT RCP - remote procedure call (RPC)) odbiorcą jest część aplikacji (w zasadzie powinienem napisać rozwiązania, aby nie wprowadzać zamieszania z ponownym użyciem słowa aplikacja w innym kontekście), która uruchamiania jest po stronie serwera i która w żaden sposób nie jest modyfikowana przez GWT. Strona serwerowa nie leży w gestii zainteresowania GWT poza udostępnieniem możliwości jej wywołania za pomocą RCP.

Przechodząc do samego tworzenia aplikacji GWT, wyróżnić należy kilka z nim związanych pojęć (patrz: Modules):
  • Moduł - module - pliki XML z konfiguracją GWT odpowiadającą jednostkom funkcjonalnym aplikacji. W module znajdują się wszystkie ustawienia konfiguracyjne niezbędne do poprawnego uruchomienia aplikacji GWT. Zalecane jest, aby moduł (plik konfiguracyjny XML aplikacji GWT) występował w pakiecie głównym projektu. Moduły definiują klasy, które podlegają translacji do JavaScript.
  • Punkt dostępowy - entry-point - jest klasą rozszerzającą interfejs EntryPoint. Podczas uruchamiania modułu, każdy punkt dostępowy jest tworzony i wywoływana jest jego metoda EntryPoint.onModuleLoad().
GWT korzysta z pojęcia pakietów do tworzenia struktury projektu. Domyślny układ pakietów GWT rozdziela części klienckie (uruchamiane w przeglądarce) od serwerowych (uruchamianych na serwerze i nie modyfikowanych przez GWT). Patrz: Project Structure.

Po lekturze wprowadzającej do GWT (Developer Guide Fundamentals) nie pozostaje nic innego jak próba zbudowania projektu GWT z użyciem M2 (najprawdopodobniej za pomocą wtyczki maven-antrun-plugin) oraz skorzystania z części serwerowej opartej o technologie przemysłowej 5-tki (Java EE 5). Już nie mogę się doczekać.

24 kwietnia 2007

Pierwsze potyczki z GWT - szczypta technologii klienckiej dla urozmaicenia

1 komentarzy
Może nie pierwsze teoretycznie, ale z pewnością pierwsze praktycznie. Po prezentacji Michała Margiela - Miesięczna ewaluacja Google Web Toolkit (GWT) - i co, fajne? - na spotkaniu Warszawa JUG myśl o konieczności samodzielnego popróbowania się z Google Web Toolkit (GWT) nie dawała mi spokoju. Jakby tego było mało natrafiłem na relację Java i AJAX w praktyce: system internetowy w GWT autorstwa lukjel. Tego było już za wiele. Czułem, że coś mnie omija. Wszyscy się dobrze bawią z GWT, a ja stoję gdzieś z boku, w kącie i zastanawiam się skąd dochodzą odgłosy zabawy ;-)

Pobrałem GWT 1.3.3 dla Windows i przeczytałem Google Web Toolkit Getting Started Guide. Niewiele tego jak na początek, pomyślałem, ale może właśnie w tym tkwi siła GWT - prostota tworzenia aplikacji, której nie ma co opisywać, bo się sama tłumaczy. I długo nie musiałem czekać, żeby się o tym przekonać (w międzyczasie lektura specyfikacji JPA zajęła mnie na trochę, więc faktycznie trwało to dłużej zanim zabrałem się za GWT).

Właśnie wczoraj, w z korespondencji otrzymałem propozycję skorzystania z akademickiej licencji dla IntelliJ IDEA. Jako członek zespołów projektów otwartych miałem już dla nich licencję, więc pomyślałem, że nawet jeśli nie skorzystam z propozycji, to może warto sprawdzić jak się sprawuje IDEA i w międzyczasie uaktualnić ją. Po aktualizacji do wersji 6.0.5 moim oczom ukazał się panel powitalny z...GWT Studio. Przypomniałem sobie o tych wszystkich intrygujących artykułach i wypowiedziach związanych z GWT i tym razem nie mogłem już odkładać tematu na później. Musiałem coś własnego zobaczyć działającego, co byłoby napisane w GWT. Zabrałem się za jego rozpracowywanie.

Nie będę reklamował komercyjnego produktu jakim jest IntelliJ IDEA, ale warto podkreślić, że cena za możliwości narzędzia jest niewspółmierna (na korzyść IDEA). Wsparcie dla GWT jest dokładnie, jakiego mógłby oczekiwać taki nowicjusz GWT jakim ja jestem. Zresztą można samemu się przekonać, obejrzawszy film - Google Web Toolkit as 1-2-3. Więcej o GWT i IDEA. Biorąc pod uwagę wsparcie dla EJB 3.0 i JPA i kilku innych technologii zdaje się, że do zbioru na codzień wykorzystywanych IDE dodam kolejne - IntelliJ IDEA. Ufff, to już 3, nie wspominając o komercyjnych ofertach od IBM (IBM Rational Application Developer 7) czy BEA (BEA Workshop Studio 3.3). I jeszcze jest Exadel Studio Pro. Każde ma coś unikatowego. Szczęśliwie ostatnie produkty to jedynie rozszerzenia dla Eclipse IDE, więc można pracować ze wszystkimi, jakby były pojedyńczym produktem.

Wracam do tematu głównego - GWT. Pierwszy strzał to oczywiście uruchomienie przykładów rozprowadzanych z GWT, które znaleźć można w katalogu samples. Dalej prześledzenie filmu o IDEA i ostatecznie pomysł, aby spróbować skorzystać z GWT i jego narzędzi do generowania struktury projektu - projectCreator oraz applicationCreator, które pozwalają na przygotowanie projektu dla Eclipse IDE. Możliwość tworzenia projektów korzystających z GWT w Eclipse IDE zmniejsza wymagania wstępne (niweluje potrzebę korzystania z IntelliJ IDEA chociażby za cenę większego wysiłku i prawdopodobnie czasu).

c:\projs> mkdir witajswieciegwt

c:\projs> cd witajswieciegwt

C:\projs\witajswieciegwt> c:\apps\gwt\projectCreator -eclipse witajswieciegwt
Created directory C:\projs\witajswieciegwt\src
Created directory C:\projs\witajswieciegwt\test
Created file C:\projs\witajswieciegwt\.project
Created file C:\projs\witajswieciegwt\.classpath

C:\projs\witajswieciegwt> c:\apps\gwt\applicationCreator -eclipse witajswieciegwt pl.jaceklaskowski.klient.WitajSwiecieGWT
'pl.jaceklaskowski.klient.WitajSwiecieGWT': Please use 'client' as the final package, as in 'com.example.foo.client.MyApp'.
It isn't technically necessary, but this tool enforces the best practice.
Google Web Toolkit 1.3.3
ApplicationCreator [-eclipse projectName] [-out dir] [-overwrite] [-ignore] className

where
-eclipse Creates a debug launch config for the named eclipse project
-out The directory to write output files into (defaults to current)
-overwrite Overwrite any existing files
-ignore Ignore any existing files; do not overwrite
and
className The fully-qualified name of the application class to create

Co za gość?! Nie mogę nazwać mojego pakietu jakbym chciał. Uroki (wymuszonej) konfiguracji domyślnej?! Oby się opłacało.

C:\projs\witajswieciegwt> c:\apps\gwt\applicationCreator -eclipse witajswieciegwt pl.jaceklaskowski.client.WitajSwiecieGWT
Created directory C:\projs\witajswieciegwt\src\pl\jaceklaskowski
Created directory C:\projs\witajswieciegwt\src\pl\jaceklaskowski\client
Created directory C:\projs\witajswieciegwt\src\pl\jaceklaskowski\public
Created file C:\projs\witajswieciegwt\src\pl\jaceklaskowski\WitajSwiecieGWT.gwt.xml
Created file C:\projs\witajswieciegwt\src\pl\jaceklaskowski\public\WitajSwiecieGWT.html
Created file C:\projs\witajswieciegwt\src\pl\jaceklaskowski\client\WitajSwiecieGWT.java
Created file C:\projs\witajswieciegwt\WitajSwiecieGWT.launch
Created file C:\projs\witajswieciegwt\WitajSwiecieGWT-shell.cmd
Created file C:\projs\witajswieciegwt\WitajSwiecieGWT-compile.cmd

Otwieram Eclipse IDE 3.3m6 i importuję projekt WitajSwiecieGWT. Po zaimportowaniu w menu Run -> Run As pojawia się nowa konfiguracja uruchomieniowa - WitajSwiecieGWT. Próba uruchomienia i okazuje się, że projekt...działa!


Wciskając przycisk Click me wywołujemy następujący kod

button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
if (label.getText().equals(""))
label.setText("Hello World!");
else
label.setText("");
}
});

, który wyświetla Hello World!.

Na zakończenie sprawdzę, jak bardzo dynamiczne jest tworzenie aplikacji z GWT i Eclipse.

Zmieniłem tekst Hello World! na Witaj Świecie GWT! i odświeżyłem przeglądarkę GWT. I znowu poprawnie. Odświeżyło się. Niestety, coś nie tak pojawiło się z kodowaniem polskich znaków. Zmieniłem Preferences->General->Workspace->Text file encoding w Eclipse na UTF-8, co wymagało również poprawienia 'Ś' w klasie i po ponownym odświeżeniu pojawiła się i polska litera.

Na koniec wykonałem skrypt WitajSwiecieGWT-compile.cmd. W wyniku uruchomienia pojawiły się dwa katalogi tomcat oraz www. Otworzyłem plik www/pl.jaceklaskowski.WitajSwiecieGWT/WitajSwiecieGWT.html w przeglądarce (Firefox 2.0.0.3 na Windows) i...znowu działa.

Szybko, sprawnie i przyjemnie. Zdecydowanie za lekko idzie ;-) Już mi się marzy budowanie za pomocą M2 z Java EE w tle.

19 kwietnia 2007

Konfiguracja wtyczek w Apache Maven 2

0 komentarzy
Oszołomiony ilością wpisów w Notatniku postanowiłem na chwilę odetchnąć i dokończyć migrację Apache OpenEJB 3 z Apache Maven 1 do Apache Maven 2. Koncepcję miałem już opracowaną od dawna, ale brakowało mi trochę wiedzy odnośnie działania Apache Maven 2 (dalej określanego jako M2). Dzisiaj przyszło mi w końcu zmagać się z tematem, który od dawna mnie nurtował, ale nigdy nie znalazłem dostatecznie dużo czasu, aby go rozpracować. Tym razem się udało i ostatecznie mogłem kolejny temat wykreślić z listy "Do Zrobienia" (ona zamiast się kurczyć to się ciągle rozszerza). Tym tematem na tapetę poszedł temat konfiguracji wtyczek w M2. Ktoś mógłby powiedzieć, że wtyczki to fundamentalne pojęcie w M2 i znajomość konfiguracji wtyczek jest niezbędna od poprawnego użycia M2, ale jak widać jestem przykładem osoby, która od lat korzysta z narzędzia i mimo, że temat konfiguracji męczył mnie od jakiegoś czasu, widać nie na tyle, aby rozpracować go raz a dobrze. Potwierdza się opinia, że korzystajmy z narzędzi, a nie uczmy się ich na wylot, ale z drugiej strony widzę jak wiele czasu straciłem na zadania, które mogłem wykonać zdecydowanie szybciej, gdybym tylko miał zacięcie do przeczytania dostępnej za darmo dokumentacji.

Do zapamiętania: Zawsze przestudiuj dostępną dokumentację *zanim* rozpoczniesz pracę z narzędziem.

Udało mi się rozpracować temat i dodatkowo spisać moje doświadczenia w kolejnym artykule zatytułowanym Konfiguracja wtyczek w Apache Maven 2. Jest to pewna realizacja pomysłu, aby wiedzę prezentować w postaci przykładów przeplatanych opisem słowno-muzycznym (mimo braku podkładu muzycznego zakładam, że treść rekompensuje stratę i zostanie mi to wybaczone). Gorąco zachęcam do lektury i nadsyłania uwag.

Pozostał jeszcze jeden temat związany z M2 - tworzenie wtyczek - i po jego zakończeniu wierzę, że będę mógł czuć się komfortowo korzystając z narzędzia. Do tej pory nie zajrzałem do darmowej książki Better Builds with Maven, która prezentuje M2 oczyma ich twórców, więc zastanawiam się, co jeszcze mnie omija podczas codziennego korzystania z niego. A co tam, najlepsze pozostawiam sobie na koniec!

13 kwietnia 2007

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

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

12 kwietnia 2007

Przewodnik o JSF i Exadel Studio Pro - pierwsze wrażenia

3 komentarzy
Właśnie ukończyłem mój pierwszy podręcznik o JSF dostarczany z Exadel Studio Pro 4.0.4. Wszystko za sprawą problemu uruchomienia pierwszej aplikacji JSF przez Ulę (aka odysse) - JSF - błąd (tagliby?). Po tygodniu bezskutecznej wymiany wiadomości postanowiłem rozeznać się w źródłach i poprosiłem odysse o przesłanie mi ich na skrzynkę. Dowiedziałem się przy tym, że odysse korzysta z Exadel Pro Studio (i wcale nie była tym zachwycona). Przyszła pora na zainstalowanie narzędzia i przekonanie się na własnej skórze co to cudeńko potrafi.

Rozpocząłem od pobrania wersji instalacyjnej Exadel Studio Pro 4.0.4 na platformę Windows. Pełna nazwa produktu to Exadel Studio Pro plug-in for Eclipse i już podczas pierwszych ekranów instalacyjnych okazało się, że będę potrzebował wersji Eclipse 3.2.x (pracuję z Eclipse 3.3M6, więc pomysł pobrania kolejnej nie był przyjęty z zachwytem). Ostatnia wersja Eclipse z serii 3.2.x to Eclipse IDE 3.2.2.

Po chwili, instalację Studio miałem za sobą.


Rzut oka na czas ewaluacji jaki mi pozostał...


i przeszedłem do lektury przewodnika Getting Started Guide for Creating a JSF Application. Wykonałem wszystkie kroki zgodnie z podręcznikiem. Tym razem postanowiłem nic nie zmieniać i pozwoliłem poprowadzić się za rękę ;-) Wszystko szło gładko, aż do momentu, kiedy należało uruchomić aplikację. Podczas uruchomienia otrzymałem znajomy zrzut wątków Javy:

Dla zainteresowanych przedstawiam go w całości:

SEVERE: Servlet.service() for servlet jsp threw exception
java.lang.IllegalStateException: Component javax.faces.component.UIViewRoot@f2225f not expected type. Expected: UIOutput. Perhaps you're missing a tag?
at com.sun.faces.taglib.html_basic.OutputTextTag.setProperties(OutputTextTag.java:90)
at javax.faces.webapp.UIComponentTag.findComponent(UIComponentTag.java:712)
at javax.faces.webapp.UIComponentTag.doStartTag(UIComponentTag.java:429)
at com.sun.faces.taglib.html_basic.OutputTextTag.doStartTag(OutputTextTag.java:155)
at org.apache.jsp.pages.greeting_jsp._jspx_meth_h_outputText_0(greeting_jsp.java:87)
at org.apache.jsp.pages.greeting_jsp._jspService(greeting_jsp.java:61)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:97)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:332)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:314)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:264)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:672)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:463)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:398)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:301)
at com.sun.faces.context.ExternalContextImpl.dispatch(ExternalContextImpl.java:322)
at com.sun.faces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:130)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:87)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:200)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:117)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:198)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:213)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:178)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:126)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:107)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:148)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:869)
at org.apache.coyote.http11.Http11BaseProtocol$Http11ConnectionHandler.processConnection(Http11BaseProtocol.java:664)
at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(PoolTcpEndpoint.java:527)
at org.apache.tomcat.util.net.LeaderFollowerWorkerThread.runIt(LeaderFollowerWorkerThread.java:80)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:684)
at java.lang.Thread.run(Unknown Source)
2007-04-12 19:02:56 org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet Faces Servlet threw exception
java.lang.IllegalStateException: Component javax.faces.component.UIViewRoot@f2225f not expected type. Expected: UIOutput. Perhaps you're missing a tag?
at com.sun.faces.taglib.html_basic.OutputTextTag.setProperties(OutputTextTag.java:90)
at javax.faces.webapp.UIComponentTag.findComponent(UIComponentTag.java:712)
at javax.faces.webapp.UIComponentTag.doStartTag(UIComponentTag.java:429)
at com.sun.faces.taglib.html_basic.OutputTextTag.doStartTag(OutputTextTag.java:155)
at org.apache.jsp.pages.greeting_jsp._jspx_meth_h_outputText_0(greeting_jsp.java:87)
at org.apache.jsp.pages.greeting_jsp._jspService(greeting_jsp.java:61)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:97)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:332)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:314)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:264)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:672)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:463)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:398)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:301)
at com.sun.faces.context.ExternalContextImpl.dispatch(ExternalContextImpl.java:322)
at com.sun.faces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:130)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:87)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:200)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:117)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:198)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:213)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:178)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:126)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:107)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:148)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:869)
at org.apache.coyote.http11.Http11BaseProtocol$Http11ConnectionHandler.processConnection(Http11BaseProtocol.java:664)
at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(PoolTcpEndpoint.java:527)
at org.apache.tomcat.util.net.LeaderFollowerWorkerThread.runIt(LeaderFollowerWorkerThread.java:80)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:684)
at java.lang.Thread.run(Unknown Source)

Zgodnie z komunikatem, okazało się że znacznik h:outputText nie jest umieszczony między znacznikami f:view (!) Zmieniłem źródło strony o dodanie znacznika

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<html>
<head>
<title></title>
</head>
<body>
<f:view>
Hello <h:outputText value="#{personBean.name}"/>!
</f:view>
</body>
</html>

i po ponownym uruchomieniu aplikacji, wpisaniu Jacek w pole tekstowe i wciśnięciu przycisku Say Hello zobaczyłem ekran z Hello Jacek!.


Zadziałało! To jest właśnie odpowiedź dla odysse - propozycja przejścia przewodnika krok po kroku. U mnie zadziałało, więc i u niej powinno.

Pomimo błędu, narzędzie jest bardzo intuicyjne i pomocne, szczególnie dla osób rozpoczynających poznawanie JSF. I jeszcze w dodatku (już prawie) darmowe! Planuję zapoznać się z pozostałymi przewodnikami udostępnymi na stronach Exadel.

11 kwietnia 2007

Adnotacja @Column i elementy precision oraz scale

0 komentarzy
Sherkan zadał ciekawe pytanie na grupie pl.comp.lang.java - EJB3.0 precyzja double. Poczatkowo wydawało się, że jest to po prostu pytanie, które wymaga doczytania specyfikacji Java Persistence API (a dokładniej części specyfikacji EJB 3.0 - JSR 220: Enterprise JavaBeans,Version 3.0 Java Persistence API, którą relacjonuję od pewnego czasu) i...wysłać odpowiedź. Podjąłem się wyzwania doczytania specyfikacji i ku mojemu zdziwieniu, okazało się, że pytanie nie należało do trywialnych (jak i pozostałe od sherkana). Tkwiło tam kilka ciekawych informacji dotyczących specyfikacji Java Persistence (JPA).

Zacznijmy od początku.

W specyfikacji JPA (rozdział 2.1.6 Mapping Defaults for Non-Relationship Fields or Properties strona 23, którego relację przedstawiłem we wpisie Java Persistence - Rozdział 2 Entities - kontynuacja lektury rozdziału) zapisano (po moim przetłumaczeniu):

Jeśli typ atrybutu (pola lub właściwości) encji należy do wymienionych typów, wtedy pole traktowane jest jakby było udekorowane adnotacją @Basic:
  • typy podstawowe w Javie
  • typy opakowujące typy podstawowe
  • java.lang.String
  • java.math.BigInteger
  • java.math.BigDecimal
  • java.util.Date
  • java.util.Calendar
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
  • byte[]
  • Byte[]
  • char[]
  • Character[]
  • typy wyliczeniowe (enums)
  • typy implementujące interfejs Serializable
Specyfikacja odwołuje się dalej do rozdziałów 9.1.18 - 9.1.21. W rozdziale 9.1.18 Basic Annotation czytamy:

Adnotacja @Basic jest najprostszym sposobem mapowania do kolumny w tablicy. Wymienione są wspomniane wyżej typy dla atrybutów trwałych, które mogą być udekorowane adnotacją @Basic. Użycie adnotacji @Basic jest opcjonalne.

Adnotacja @Basic nie prowadzi nas do żadnej odpowiedzi na pytanie sherkana, której szukać należy w innej adnotacji - @Column.

Adnotacja @Column opisana jest w rozdziale 9.1.5 Column Annotation, gdzie można przeczytać:

Adnotacja @Column jest używana do określenia kolumny i jej właściwości dla atrybutu trwałego.

Adnotacja @Column składa się z wielu elementów, z których na uwagę (w celu udzielenia odpowiedzi sherkanowi) zasługują następujące:
  • String columnDefinition() default "";
  • int precision() default 0;
  • int scale() default 0;
3 wyżej wymienione elementy uściślają mapowanie między klasą encji a bazą danych. Należy rozważyć kilka przypadków ze względu na istnienie schematu bazodanowego odpowiadającego encji - struktury bazodanowe już istnieją, będą tworzone podczas uruchamiania aplikacji bądź będą modyfikowane na potrzeby aplikacji. Mimo, że specyfikacja wyraźnie nadmienia (rozdział 9 Metadata for Object/Relational Mapping strona 163), że:

Mechanizm tworzenie skryptów DDL jest opcjonalną funkcjonalnością dostawcy trwałości. W celu zachowania pełnej zgodności ze specyfikacją aplikacja nie powinna polegać na niej.

Mechanizm generowania DDL stał się de facto standardem i każdy dostawca trwałości udostępnia go poprzez konfigurację jednostki utrwalania w persistence.xml (plik konfiguracyjny dostawcy trwałości).

Zajmijmy się przypadkiem, kiedy schemat bazy danych jest tworzony (bądź modyfikowany) na podstawie modelu obiektowego. Przypadek istnienia schematu bazy danych nie jest interesujący w tym kontekście, gdyż wszystkie cechy tabel są już ustalone.

Problem: Chcielibyśmy, aby liczby zmiennoprzecinkowe były zapisywane w postaci - 5 cyfr całkowitych (przed przecinkiem) oraz 2 cyfry ułamkowe (po przecinku), np. liczba 12345,6789 byłaby zapisana jako 12345,67.

Oczywiście chcielibyśmy, aby liczba 123456,789 powodowała pojawienie się wyjątku niezgodności zakresu, ale...należy przypomnieć sobie rolę JPA. JPA jest to most pomiędzy danymi obiektowymi a relacyjnymi i niczym więcej (może poza drobnymi dodatkami). Nie ma mowy o kontroli zakresów - nie jest to częścią JPA. Konwersja odbywa się albo na poziomie kodu w Javie (zgodność typu i ich zakresu), albo na poziomie bazy danych, gdzie następuje wprowadzenie danych spoza zakresu i ewentualne zgłoszenie wyjątku. JPA jedynie raportuje błędy, których nie była źródłem, bo pełni jedynie rolę pośrednika (jedynie jest uzasadnione jedynie w tym zdaniu ;-)).

Zanim przejdziemy do przykładów odszukajmy informacji dotyczących elementów adnotacji @Column:
  • int precision - (domyślnie 0) - element opcjonalny określający rozmiar kolumny liczbowej dla typu zmiennoprzecinkowego (o dokładnej wartości), tj. ilość cyfr reprezentujących całą liczbę łącznie z jej opcjonalną częścią ułamkową.
  • int scale - (domyślnie 0) - element opcjonalny określający ilość cyfr po przecinku dla typu zmiennoprzecinkowego (o dokładnej wartości)
  • String columnDefinition - (domyślnie "" - łańcuch pusty) - element opcjonalny określający fragment SQL, który tworzy definicję pola w skryptach tworzących schemat (DDL).
Na szczególną uwagę zasługuje przypis (a już miałem napisać adnotacja) - o dokładnej wartości. Nigdy nie zajmowałem się tymi zagadnieniami, więc przeszukałem Internet w celu znalezienia odpowiedzi co to są liczby o dokładnej wartości.

Dobre wytłumaczenie znalazłem na stronie dokumentacji bazy MySQL - 23.1. Types of Numeric Values i doczytałem, że liczby o dokładnej wartości to 3,14 podczas, gdy ta sama liczba zapisana w postaci 3,14E0 jest już liczbą przybliżoną. Ważne, aby pamiętać, że podanie liczby w postaci skończonego ciągu cyfr (możliwie z przecinkiem) jest liczbą dokładną, a jakakolwiek jej reprezentacja naukowa z mantysą i wykładnikiem jest już jej przybliżeniem.

Zostawiając naukowe wywody na boku, przypomnę o starej, dobrej specyfikacji, która pozwoliła na dostęp do bazy danych na początku pojawienia się tego problemu w Javie - Java Database Connectivity (JDBC). JDBC jest protoplastą JPA. Zmęczeni ciągłym operowaniem klasami JDBC, Hibernate spowodował, że połączenie świata obiektowego i relacyjnego stało się niezwykle proste. Po drodze mieliśmy nietrafioną specyfikację EJB 2.1 wraz z mapowaniem klas na ich reprezentację relacyjną - komponenty encyjne CMP, co przyczyniło się do migracji programistów do Hibernate i tym podobnych rozwiązań, aż w końcu dotarliśmy do EJB 3.0 z JPA. Trochę historii nie zaszkodzi, bo pozwala na zrozumienie aktualnych rozwiązań i odszukanie odpowiedzi. Wracamy, więc do korzeni, czyli JDBC. A dlaczego, ktoś zapyta? Bo podstawą dla działania JPA jest właśnie JDBC i zrozumienie JDBC to początek zrozumienia JPA (być może i na odwrót, ale nie polecam).

Odszukując informacji dotyczących mapowania typów bazodanowych na typy w Javie natrafiłem na dokument Mapping SQL and Java Types - część dokumentacji Java SE 5, w którym czytamy, że rekomendowanym typem w Javie dla typu bazodanowego DOUBLE jest double (8.3.9 DOUBLE) podczas, gdy dla DECIMAL lub NUMERIC będzie to java.math.BigDecimal (8.3.11 DECIMAL and NUMERIC).

Zrozumienie mapowania typów bazodanowych na odpowiadające im typy w Javie to zrozumienie połowy, jeśli nie całego, tematu. Pamiętając, że JPA stanowi jedynie pomost między modelem obiektowym a relacyjnym, a rolę kontrolera typu i zakresu danych przejmuje baza danych, koniecznie należałoby zapoznać się z możliwościami konkretnej bazy danych odnośnie wspomnianych typów bazodanowych - DECIMAL, NUMERIC oraz DOUBLE.

Rozpatrując bazę danych Apache Derby dokumentacja dotycząca wspieranych typów danych - Data types - prezentuje ich możliwości:
  • DECIMAL(precision [, scale]) jest typem o dokładnej wartości i odpowiadającym jej typem w Javie jest java.math.BigDecimal.
  • NUMERIC jest synonimem dla DECIMAL
  • DOUBLE jest synonimem dla DOUBLE PRECISION
  • DOUBLE PRECISION jest typem o przybliżonej wartości, dla której typem w Javie jest java.lang.Double.
Przeglądając dokumentację MySQL w rozdziale 11.2. Numeric Types czytamy opis podobny do dokumentacji Apache Derby, ale na uwagę zasługuje notka:

For maximum portability, code requiring storage of approximate numeric data values should use FLOAT or DOUBLE PRECISION with no specification of precision or number of digits.

, czyli stosowanie precyzji oraz skali dla liczb przybliżonych zmiennoprzecinkowych jest nierekomendowane oraz kolejna ciekawa adnotacja^H^H^Hnotka:

The DECIMAL and NUMERIC data types are used to store exact numeric data values. In MySQL, NUMERIC is implemented as DECIMAL. These types are used to store values for which it is important to preserve exact precision, for example with monetary data.

, co oznacza, że DECIMAL oraz NUMERIC są idealnymi kandydatami do przechowywania dokładnych wartości liczb zmiennoprzecinkowych, np. w zastosowaniach finansowych.

Zapoznając się z dokumentacją PostgreSQL czytamy w rozdziale 8.1. Numeric Types jest bardzo zbliżona do definicji typów w MySQL oraz Derby.

Istotne jest, aby pamiętać, że zaczynamy wkraczać na grunt nieustandaryzowany, tj. istnieje standard SQL-92, który jest standardem dla typów w bazach danych, jednakże istnieją rozszerzenia, które mogą udoskonalić nasz model relacyjny i z których warto czasami korzystać (chociaż okazuje się, że w tym konkretnym przypadku bazy otwarte są zgodne). Własnie z tego powodu - istnienia rozszerzeń - dodano element columnDefinition do adnotacji @Column. Jeśli chcielibyśmy wpłynąć na ostateczne zapytanie SQL, które będzie tworzyło, zapisywało, czy odczytywało dane z/do bazy danych możemy skorzystać z elementu columnDefinition. Przy jego zastosowaniu nie ma znaczenia domyślne mapowanie typów w Javie na odpowiadające im typy w bazie danych, ponieważ są one nadpisywane przez własną definicję pola określoną w elemencie columnDefinition.

Biorąc pod uwagę dyskusję jaka rozpoczęła się na grupie Apache OpenJPA dev nt. temat - @Column with precision and scale - how does it work? okazuje się, że nawet zastosowanie typu BigDecimal, aby wymusić zastosowanie precision oraz scale nie ma zastosowania (błąd w implementacji? - OPENJPA-213 @Column with precision and scale should result in NUMERIC(precision, scale)). Zresztą zaraz się o tym przykonamy.

Na koniec warto zaznajomić się z dokumentacja samego typu java.math.BigDecimal. Na uwagę zasługuje fakt, że nie istnieje konstruktor, który utworzyłby liczbę typu BigDecimal z podanymi precyzją oraz skalą. Dziwne?!

Przyjrzyjmy się kilku przykładom i ich działaniu z dostawcami Apache OpenJPA 0.9.7-SNAPSHOT, TopLink Essentials 2.0 BUILD 40 oraz Hibernate EntityManager 3.3.1.

Załóżmy, że zdefiniowaliśmy encję PracownikSpecjalny (która rozszerza encję Osoba) z atrybutem pensja typu double.

@Entity
public class PracownikSpecjalny extends Osoba {

private double pensja;

public double getPensja() {
return pensja;
}

public void setPensja(double pensja) {
this.pensja = pensja;
}
}

Dla Apache OpenJPA jest to informacja, aby stworzyć następujące zapytanie SQL:

157 derbyPU INFO [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.DerbyDictionary".
...
3141 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 18662247 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

TopLink Essentials utworzy następujące zapytanie:

[TopLink Info]: 2007.04.11 11:37:02.890--ServerSession(18926678)--Thread(Thread[main,5,main])--TopLink, version: Oracle TopLink Essentials - 2.0 (Build 40 (03/30/2007))
[TopLink Fine]: 2007.04.11 11:37:04.593--Thread(Thread[main,5,main])--Detected Vendor platform: oracle.toplink.essentials.platform.database.JavaDBPlatform
...
[TopLink Fine]: 2007.04.11 11:37:05.078--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA FLOAT, PRIMARY KEY (NUMER))

natomiast Hibernate EntityManager najpierw odmówił posłuszeństwa, kiedy zdefiniowałem parametr

<property name="hibernate.use_sql_comments" value="true"/>

w persistence.xml, co spowodowało utworzenie komentarzy w zapytaniach

11:38:27,578 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
11:38:27,578 DEBUG SQL:393 -
/* insert pl.jaceklaskowski.jpa.entity.Projekt
*/ insert
into
Projekt
(chair_numer, rodzajProjektu, nazwa)
values
(?, ?, ?)
Hibernate:
/* insert pl.jaceklaskowski.jpa.entity.Projekt
*/ insert
into
Projekt
(chair_numer, rodzajProjektu, nazwa)
values
(?, ?, ?)
11:38:27,578 DEBUG AbstractBatcher:476 - preparing statement
11:38:27,593 DEBUG JDBCExceptionReporter:69 - could not insert: [pl.jaceklaskowski.jpa.entity.Projekt] [/* insert pl.jaceklaskowski.jpa.entity.Projekt */ insert into Projekt (chair
_numer, rodzajProjektu, nazwa) values (?, ?, ?)]
ERROR 42X01: Syntax error: Encountered "/" at line 1, column 1.
at org.apache.derby.iapi.error.StandardException.newException(Unknown Source)
at org.apache.derby.impl.sql.compile.ParserImpl.parseStatement(Unknown Source)
at org.apache.derby.impl.sql.GenericStatement.prepMinion(Unknown Source)
at org.apache.derby.impl.sql.GenericStatement.prepare(Unknown Source)
at org.apache.derby.impl.sql.conn.GenericLanguageConnectionContext.prepareInternalStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement.(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement20.(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement30.(Unknown Source)
at org.apache.derby.jdbc.Driver30.newEmbedPreparedStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedConnection.prepareStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedConnection.prepareStatement(Unknown Source)
at org.hibernate.jdbc.AbstractBatcher.getPreparedStatement(AbstractBatcher.java:497)
at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:94)
at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:87)
at org.hibernate.jdbc.AbstractBatcher.prepareBatchStatement(AbstractBatcher.java:218)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2220)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2656)
at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:52)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:248)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:232)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:139)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:54)
at pl.jaceklaskowski.jpa.BaseTest.utworzPracownikow(BaseTest.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.testng.internal.MethodHelper.invokeMethod(MethodHelper.java:552)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:411)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:785)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:114)
at org.testng.TestRunner.privateRun(TestRunner.java:693)
at org.testng.TestRunner.run(TestRunner.java:574)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:241)
at org.testng.SuiteRunner.run(SuiteRunner.java:145)
at org.testng.TestNG.createAndRunSuiteRunners(TestNG.java:901)
at org.testng.TestNG.runSuitesLocally(TestNG.java:863)
at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.executeTestNG(TestNGDirectoryTestSuite.java:195)
at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.execute(TestNGDirectoryTestSuite.java:133)
at org.apache.maven.surefire.Surefire.run(Surefire.java:132)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)
at org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:818)

, a po usunięciu parametru pojawiło się upragnione zapytanie SQL tworzące tabelę dla encji Osoba:

11:46:01,734 INFO Dialect:152 - Using dialect: org.hibernate.dialect.DerbyDialect
...
11:46:40,359 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja double,
tytul varchar(255),
primary key (numer)
)

Jak widać dla TopLinka pensja stała się kolumną o typie FLOAT, gdzie dla OpenJPA oraz Hibernate jest to DOUBLE. Jeden z detali implementacyjnych do zanotowania.

Sprawdźmy jak zachowają się dostawcy JPA po skorzystaniu z adnotacji @Column i elementami precision oraz scale. Udekorujmy atrybut pensja adnotacją @Column.

@Column(precision = 5, scale = 2)
public double getPensja() {
return pensja;
}

Okazuje się, że nie ma to żadnego wpływu na skrypty SQL tworzone przez dostawców.

Skorzystajmy zatem z elementu columnDefinition.

@Column(precision = 5, scale = 2, columnDefinition = "DECIMAL(7,3)")
public double getPensja() {
return pensja;
}

, czyli pensja będzie 7-cyfrową liczbą z 3 liczbami po przecinku.
Apache OpenJPA

2938 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 21781303 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DECIMAL(7,3), tytul VARCHAR(255), PRIMARY KEY (numer))

TopLink Essentials:

[TopLink Fine]: 2007.04.11 11:54:02.578--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL(7,3), PRIMARY KEY (NUMER))

oraz Hibernate EntityManager

11:55:00,000 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja DECIMAL(7,3),
tytul varchar(255),
primary key (numer)
)

, czyli wszyscy dostawcy skorzystali z konfiguracji kolumny poprzez element columnDefinition adnotacji @Column.

Sprawdźmy w jaki sposób dostawcy zareagują na atrybut pensja o typie java.math.BigDecimal.

public BigDecimal getPensja() {
return pensja;
}

OpenJPA:

2953 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 21781303 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

czyli ujawnia się błąd OpenJPA, który nie toleruje BigDecimal (przynajmniej dla Derby).
TopLink:

[TopLink Fine]: 2007.04.11 12:05:47.593--ServerSession(18926678)--Connection(30987167)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL, PRIMARY KEY (NUMER))

oraz Hibernate:

create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja numeric(19,2),
tytul varchar(255),
primary key (numer)
)

Jak widać dowolność w interpretacji typu BigDecimal jest ogromna.

Na koniec sprawdźmy jak zachowają się nasi dostawcy JPA z typem BigDecimal oraz adnotacją @Column z elementami precision oraz scale.

@Column(precision = 7, scale = 3)
public BigDecimal getPensja() {
return pensja;
}

OpenJPA:

2968 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 18662247 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

OpenJPA upiera się przy swoim i nie reaguje na elementy precision oraz scale adnotacji @Column.
TopLink:

[TopLink Fine]: 2007.04.11 12:10:09.281--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL(7,3), PRIMARY KEY (NUMER))

oraz Hibernate

12:28:47,640 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja numeric(7,3),
tytul varchar(255),
primary key (numer)
)

Poza OpenJPA wszyscy zachowali się zgodnie z oczekiwaniem, tj. elementy precision oraz scale adnotacji @Column dla atrybutu o typie BigDecimal wpłynęły na ostateczny skrypt tworzący tabelę Osoba.

Dzięki Sherkan za temat!

09 kwietnia 2007

Java Persistence - 4.7 GROUP BY, HAVING oraz 4.8 Klauzula SELECT

0 komentarzy
Święta, święta i po świętach. Kilka dni wolnych i ponownie zaciągam pasa i wracam do pracy. Tym razem relacja z dwóch rozdziałów specyfikacji Java Persistence - 4.7 GROUP BY, HAVING oraz 4.8 Klauzula SELECT. Kolejne 4 strony specyfikacji za nami! Tylko 4, a ile nowinek. Aż boję się pomyśleć, ile jeszcze przede mną.

4.7 GROUP BY, HAVING

Konstrukcja GROUP BY umożliwia pogrupowanie wartości według zbioru właściwości. Konstrukcja HAVING umożliwia określenie dodatkowych ograniczeń, które zawężają wynik zapytania. Ograniczenia działają na grupach będących wynikiem działania GROUP BY.

Składnia klauzul GROUP BY oraz HAVING jest następująca:

klauzula_groupby ::= GROUP BY składnik_groupby {, składnik_groupby}*
składnik_groupby ::= wyrażenie_ścieżkowe_o_pojedyńczej_wartości | zmienna_identyfikacyjna
klazula_having ::= HAVING wyrażenie_warunkowe

Jeśli zapytanie zawiera klauzule WHERE oraz GROUP BY, najpierw wykonywana jest klauzula WHERE, po której następuje utworzenie group i odrzucenie tych, które nie spełniają warunku klauzuli HAVING.

Wymagania dla klauzuli SELECT, kiedy używana jest klauzula GROUP BY, są analogiczne do wymagań SQL: dowolny element występujący w klauzuli SELECT (inny niż parametr wejściowy funkcji agregującej) musi również występować w klauzuli GROUP BY. W tworzeniu group, wartości null są traktowane jako jednakowe, tj. należące do tej samej grupy.

W przypadku grupowania po encji, encja nie może zawierać serializowanych pól stanowych lub pól stanowych opartych o LOB (jednakże, co to znaczy grupowanie po encji nadal jest dla mnie niezrozumiałe - odkładam do wyjaśnienia na kolejne strony specyfikacji).

Klauzula HAVING musi definiować kryteria wyszukiwania względem grup lub funkcji agregujących.

Jeśli nie istnieje klauzula GROUP BY i korzysta się z klauzuli HAVING, wynik zapytania traktowany jest jako pojedyńcza grupa, i klauzula SELECT może jedynie zawierać funkcje agregujące. Dostawca trwałości nie jest zobowiązany do wspierania zapytań z klauzulą HAVING przy braku GROUP BY. Przenośne aplikacje nie powinny polegać na HAVING bez użycia GROUP BY.

Query query = em
.createQuery("SELECT p.rodzajProjektu, COUNT(p) FROM Projekt p GROUP BY p.rodzajProjektu ORDER BY p.rodzajProjektu");
List<Object[]> projekty = query.getResultList();
assert projekty.size() == 2 : projekty.size();
assert projekty.get(0)[0] == RodzajProjektu.OTWARTY &&amp; ((Long) projekty.get(0)[1]) == 3 :
"Oczekiwano 3 projektów otwartych, a otrzymano " + projekty.get(0)[1] + " projekty typu " + projekty.get(0)[0];
assert projekty.get(1)[0] == RodzajProjektu.KOMERCYJNY &&amp; ((Long) projekty.get(1)[1]) == 1 :
"Oczekiwano 1 projektu komercyjnego, a otrzymano " + projekty.get(1)[1] + " projekt(y) typu " + projekty.get(1)[0];

Specyfikacja przedstawia dwa przykłady zapytań z wykorzystaniem GROUP BY oraz HAVING.
Pierwsze zapytanie służy do pobrania statusu klientów, średniej ilości złożonych zamówień klientów z danym statusem oraz ich ilości pogrupowanych po statusie klientów (klauzula GROUP BY) dla statusu oznaczonego jako 1 lub 2 (klauzula HAVING).
Drugie zapytanie zwraca kraj oraz ilość klientów dla danego kraju, w których ilość klientów przekracza 3.

4.8 Klauzula SELECT

Klauzula SELECT wyznacza wynik zapytania. W wyniku zapytania możemy otrzymać pojedyńczą encję, listę encji, pojedyńczą wartość bądź tablicę wartości, wliczając w to tablicę wartości różnych typów.

Klauzula SELECT może zawierać:
  • Wyrażenie ścieżkowe o pojedyńczej wartości (ang. single valued path expression)
  • Wyrażenie agregujące (ang. aggregate expression), tj. działające na grupie wartości
  • Zmienną identyfikującą (ang. identification variable)
  • Wyrażenie OBJECT
  • Wyrażenie tworzące/egzemplarzowe (ang. constructor expression)
lub ich kombinację.

Moją uwagę zwrócił element - wyrażenie tworzące/egzemplarzowe. Nigdy wcześniej nie spotkałem się z tym określeniem (tak przy okazji, nie widziałem tego również w Hibernate). Więcej za moment wraz z przykładami.

Należy zauważyć, że klauzula SELECT musi składać się wyłącznie z wyrażeń, które w wyniku zwracają pojedyńcze wartości. Niedozwolone jest zbudowanie zapytania, którego klauzula SELECT zawierałaby wyrażenie stworzone w ten sposób:

SELECT o.projekty FROM Osoba o

Wykonanie powyższego zapytania z Apache OpenJPA 0.9.7-SNAPSHOT zakończy się niepowodzeniem z następującym komunikatem:

<0.9.7-incubating-SNAPSHOT nonfatal user error> org.apache.openjpa.persistence.ArgumentException:
Query projections cannot include array, collection, or map fields. Invalid query:
"SELECT o.projekty FROM Osoba o"

Pole projekty jest polem relacji wiele-do-wielu i jest kolekcją encji, co jest niedozwolone.

Powyższe zapytanie zapisane z wykorzystaniem łączenia jest już całkowicie legalne i zwróci listę projektów, do których przypisane są osoby (projekty mogą być powielone).

SELECT p FROM Osoba o, IN(o.projekty) p

Słowo kluczowe DISTINCT wskazuje, aby powielone wartości zostały usunięte z wyniku zapytania.

SELECT DISTINCT p FROM Osoba o, IN(o.projekty) p

Samodzielne zmienne identyfikujące w klauzuli SELECT mogą opcjonalnie być parametrem wejściowym operatora OBJECT (co nie zmienia semantyki zapytania).

SELECT DISTINCT OBJECT(p) FROM Osoba o, IN(o.projekty) p

Zabronione jest korzystanie z OBJECT, aby oznaczać wyrażenia ścieżkowe.
Poniższe zapytanie jest nieprawidłowe ze względu na użycie wyrażenia ścieżkowego z OBJECT.

SELECT DISTINCT OBJECT(p.nazwa) FROM Osoba o, IN(o.projekty) p


4.8.1 Typ wyniku klauzuli SELECT

Typ wyniku zapytania określonego przez klauzulę SELECT jest typem abstrakcyjnego schematu (AST) encji, typem pola-stanu, wynikiem funkcji agregującej, wynikiem operacji tworzącej egzemplarz encji, lub ich kombinacją.

Typ wynikowy klauzuli SELECT jest wyznaczany przez typy elementów składowych klauzuli. Wiele elementów składa się na tablicę Object[], której elementy są ułożone zgodnie z kolejnością odpowiadających im wyrażeń w klauzuli SELECT.

Query query = em.createQuery("SELECT o, COUNT(o.projekty) FROM Osoba o JOIN o.projekty p GROUP BY o");
List<Object[]> resultQuery = query.getResultList();
assert resultQuery.size() == 3 :
"Oczekiwano 3 projektów (unikatowych) z uczestnikami podczas, gdy zwrócono " + resultQuery.size();
final Object[] firstElementInQueryResult = resultQuery.get(0);
assert firstElementInQueryResult.length == 2 :
"Oczekiwano 2 elementów w tablicy, a otrzymano " + firstElementInQueryResult.length;
final Object firstElement = resultQuery.get(0)[0];
assert firstElement instanceof Osoba :
"Oczekiwano, że pierwszy element w tablicy wyników jest typu Osoba, a otrzymano typ " +
firstElement.getClass().getCanonicalName();
final Object secondElement = resultQuery.get(0)[1];
assert secondElement instanceof Long :
"Oczekiwano, że drugi element w tablicy wyników jest typu Long, a otrzymano typ " + secondElement.getClass().getCanonicalName();

Typ wyniku wyrażenia składowego w klauzuli SELECT jest wyliczany następująco:

  • Wyrażenie ścieżkowe o pojedyńczej wartości, które jest wyrażeniem ścieżkowym pola stanu skutkuje w egzemplarzu tego samego typu jak odpowiadający pole-stanu encji. Jeśli pole stanu encji jest typem podstawowym, odpowiadający typ obiektowy jest zwracany.

    Query query = em.createQuery("SELECT o.numer, o.imie, o.nazwisko FROM Osoba o");
    List<Object[]> resultQuery = query.getResultList();
    assert resultQuery.size() == 4 : "Oczekiwano 4 osób, a zwrócono " + resultQuery.size();
    final Object[] firstElementInQueryResult = resultQuery.get(0);
    assert firstElementInQueryResult.length == 3 : "Oczekiwano 3 elementów w tablicy, a otrzymano " + firstElementInQueryResult.length;
    final Object firstElement = resultQuery.get(0)[0];
    assert firstElement instanceof Long : "Oczekiwano, że pierwszy element w tablicy wyników jest typu Long, a otrzymano typ " + firstElement.getClass().getCanonicalName();
    final Object secondElement = resultQuery.get(0)[1];
    assert secondElement instanceof String : "Oczekiwano, że drugi element w tablicy wyników jest typu String, a otrzymano typ " + secondElement.getClass().getCanonicalName();
    final Object thirdElement = resultQuery.get(0)[2];
    assert thirdElement instanceof String : "Oczekiwano, że trzeci element w tablicy wyników jest typu String, a otrzymano typ " + secondElement.getClass().getCanonicalName();

  • Wyrażenie ścieżkowe o pojedyńczej wartości, które jest wyrażeniem ścieżkowym asocjacji o pojedyńczej wartości skutkuje w egzemplarzu encji (pod)typu pola-asocjacji.

    Query query = em.createQuery("SELECT p.nazwa, p.chair FROM Projekt p");
    List<Object[]> resultQuery = query.getResultList();
    final Object[] firstElementInQueryResult = resultQuery.get(0);
    assert firstElementInQueryResult.length == 2 : "Oczekiwano 2 elementów w tablicy, a otrzymano " + firstElementInQueryResult.length;
    final Object firstElement = resultQuery.get(0)[0];
    assert firstElement instanceof String : "Oczekiwano, że pierwszy element w tablicy wyników jest typu String, a otrzymano typ " + firstElement.getClass().getCanonicalName();
    final Object secondElement = resultQuery.get(0)[1];
    assert secondElement instanceof Osoba : "Oczekiwano, że drugi element w tablicy wyników jest typu Osoba, a otrzymano typ " + secondElement.getClass().getCanonicalName();

  • Typ wynikowy zmiennej identyfikacyjnej jest (pod)typem encji, na którą wskazuje zmienna.

    Query query = em.createQuery("SELECT p, o FROM Projekt p, IN(p.chair) o");
    List<Object[]> resultQuery = query.getResultList();
    final Object[] firstElementInQueryResult = resultQuery.get(0);
    assert firstElementInQueryResult.length == 2 : "Oczekiwano 2 elementów w tablicy, a otrzymano " + firstElementInQueryResult.length;
    final Object firstElement = resultQuery.get(0)[0];
    assert firstElement instanceof Projekt : "Oczekiwano, że pierwszy element w tablicy wyników jest typu Projekt, a otrzymano typ " + firstElement.getClass().getCanonicalName();
    final Object secondElement = resultQuery.get(0)[1];
    assert secondElement instanceof Osoba : "Oczekiwano, że drugi element w tablicy wyników jest typu Osoba, a otrzymano typ " + secondElement.getClass().getCanonicalName();

  • Typ wyniku wyznaczonego przez wyrażenie agregujące opisany jest poniżej.

  • Typ wyniku wyznaczonego przez wyrażenie tworzące jest typu, dla którego wywoływany jest konstruktor. Typy parametrów wejściowych konstruktora są wyznaczane zgodnie z powyższymi regułami.

4.8.2 Wyrażenie tworzące w klauzuli SELECT

Klauzula SELECT może zawierać wyrażenie tworzące będące wywołaniem konstruktora. Klasa, do której należy konstruktor nie musi być klasą encji lub być zmapowana w jakikolwiek sposób do bazy danych. Nazwa konstruktora musi być kwalifikowana, tj. poprzedzona nazwą pakietu, do której należy.

Składnia wyrażenia tworzącego jest następująca:

wyrażenie_tworzące ::= NEW nazwa_konstruktora( parametr_wejściowy {,parametr_wejściowy})
parametr_wejściowy ::= wyrażenie_ścieżkowe_o_pojedyńczej_wartości | wyrażenie_agregujące

Przykład: Zapytanie, w którym tworzone są egzemplarze PewnaKlasa dla każdego rekordu korzystając z 4-argumentowego konstruktora.

Query query = em
.createQuery("SELECT NEW pl.jaceklaskowski.jpa.PewnaKlasa(p.nazwa, o.imie, o.nazwisko, o.numer), o, p.nazwa FROM Projekt p JOIN p.chair o");
List<Object[]> resultQuery = query.getResultList();
final Object[] firstElementInQueryResult = resultQuery.get(0);
assert firstElementInQueryResult.length == 3 : "Oczekiwano 3 elementów w tablicy, a otrzymano " + firstElementInQueryResult.length;
final Object firstElement = resultQuery.get(0)[0];
assert firstElement instanceof PewnaKlasa : "Oczekiwano, że pierwszy element w tablicy wyników jest typu PewnaKlasa, a otrzymano typ " + firstElement.getClass().getCanonicalName();
final Object secondElement = resultQuery.get(0)[1];
assert secondElement instanceof Osoba : "Oczekiwano, że drugi element w tablicy wyników jest typu Osoba, a otrzymano typ " + secondElement.getClass().getCanonicalName();
final Object thirdElement = resultQuery.get(0)[2];
assert thirdElement instanceof String : "Oczekiwano, że trzeci element w tablicy wyników jest typu String, a otrzymano typ " + thirdElement.getClass().getCanonicalName();

UWAGA: TopLink Essentials 2.0 BUILD 40 pozwala na korzystanie z wyrażenia tworzącego, które korzysta ze zmiennych identyfikujących (co jest niezgodne ze specyfikacją, ale nie zabronione).

UWAGA: Apache OpenJPA 0.9.7-SNAPSHOT nie wykona poprawnie powyższego przykładu ze względu na błąd analizy zapytania, które posiadania wiele wyrażeń w klauzuli SELECT z wyrażeniem egzemplarzowym.

Jeśli konstruktor należy do encji, zwrócone encje będą w stanie NOWY (w przeciwieństwie do innych zapytań, w których encje są w stanie ZARZĄDZANY).

{
Query query = em.createQuery("SELECT NEW pl.jaceklaskowski.jpa.entity.Osoba(o.imie, o.nazwisko) FROM Projekt p JOIN p.chair o");
List<Osoba> osoby = query.getResultList();
final Osoba osoba = osoby.get(0);
assert !em.contains(osoba) : "Egzemplarz encji nie może być zarządzany przez zarządcę trwałości";
}
{
Query query = em.createQuery("SELECT o FROM Projekt p JOIN p.chair o");
List<Osoba> osoby = query.getResultList();
final Osoba osoba = osoby.get(0);
assert em.contains(osoba) : "Egzemplarz encji musi być zarządzany przez zarządcę trwałości";
}

4.8.3 Wartości NULL w wyniku zapytania

Jeśli wynik zapytania korzysta z pola-asocjacji lub pola-stanu, którego wartość jest NULL, wartość NULL jest zwrócona w wyniku. Konstrukcja IS NOT NULL służy do usunięcia wartości NULL z wyniku zapytania.

Specyfikacja zwraca uwagę na pola-stanu, których typem jest numeryczny typ prosty w Javie, które nie przyjmują wartości NULL. Niedozwolone jest, aby zapytanie korzystające z takich pól zwróciło wartość NULL.

4.8.4 Funkcje agregujące w klauzuli SELECT

Wynikiem zapytania może być wynik funkcji agregującej przyłożonej do wyrażenia ścieżkowego.

Funkcje agregujące mogące występować w klauzuli SELECT to AVG, COUNT, MAX, MIN, SUM.

Wszystkie funkcje agregujące, poza COUNT, wymagają, aby ich argumentem wejściowym było wyrażenie ścieżkowe, które kończy się polem-stanu. Wyrażenie ścieżkowe będące parametrem wejściowym dla COUNT może kończyć się polem-stanu, polem-asocjacji, lub zmienną identyfikującą.

Parametrem wejściowym dla funkcji SUM oraz AVG muszą być typu liczbowego. Parametry wejściowe dla MAX oraz MIN muszą odpowiadać polom-stanu o typie porządkowym, tj. mogącym się uporządkować - typy liczbowe, łańcuchowe, znakowe oraz kalendarzowe.

Typy zwracane w zapytaniu z funkcjami agregującymi:
  • COUNT zwraca Long
  • MAX, MIN zwraca typ pola-stanu parametru wejściowego
  • AVG zwraca Double
  • SUM zwraca Long, jeśli pole-stanu jest typu liczbowego (poza BigInteger); Double, jeśli pole-stanu jest typu zmiennoprzecinkowego; BigInteger, jeśli pole-stanu jest typu BigInteger; BigDecimal, jeśli pole-stanu jest typu BigDecimal.
W przypadku braku wartości, z których mogłyby skorzystać funkcje SUM, AVG, MAX, MIN, zwracany jest NULL.

Query query = em
.createQuery("SELECT MAX(o.dzienImienin) FROM Osoba o WHERE o.imie = :imie AND o.imie <> :imie");
query.setParameter("imie", "Jacek");
Calendar calendar = (Calendar) query.getSingleResult();
assert calendar == null : "Nie istnieje osoba, która spełnia warunek w klauzuli WHERE, MAX musi zwrócić NULL, a otrzymano " + calendar;

W przypadku braku wartości, z których mogłaby skorzystać funkcja COUNT, zwracane jest 0.

Query query = em
.createQuery("SELECT COUNT(o.dzienImienin) FROM Osoba o WHERE o.imie = :imie AND o.imie <> :imie");
query.setParameter("imie", "Jacek");
long liczbaOsob = (Long) query.getSingleResult();
assert liczbaOsob == 0 : "Nie istnieje osoba, która spełnia warunek w klauzuli WHERE, COUNT musi zwrócić 0, a otrzymano " + liczbaOsob;

Argument funkcji agregującej może być poprzedzony słowem kluczowym DISTINCT, aby usunąć powielone wartości przed faktycznym wywołaniem funkcji. Zastosowanie DISTINCT w przypadku MAX oraz MIN jest dozwolone, ale nie wpływa na wynik.

query = em.createQuery("SELECT COUNT(p) FROM Osoba o JOIN o.projekty p");
liczbaProjektow = (Long) query.getSingleResult();
assert liczbaProjektow == 7 : "Oczekiwano 7 powielonych projektów, które są przypisane do osób, a otrzymano " + liczbaProjektow;

query = em.createQuery("SELECT COUNT(DISTINCT p) FROM Osoba o JOIN o.projekty p");
liczbaProjektow = (Long) query.getSingleResult();
assert liczbaProjektow == 3 : "Oczekiwano 3 unikatowych projektów, które są przypisane do osób, a otrzymano " + liczbaProjektow;

Wartości NULL są eliminowane zanim uruchomiona zostanie funkcja agregująca, bez względu na użycie DISTINCT. Jest to szczególnie istotne przy funkcji COUNT, gdzie wywołanie COUNT(o.dzienImienin) zliczy wyłącznie osoby z niepustym (nie NULL) dniem imienin podczas, gdy COUNT(o) zlicza wszystkie encje bez względu na wartości pól trwałych. Rozwiązaniem jest zastosowanie IS NOT NULL w klauzuli WHERE (wspaniale przedstawia to ostatni z przykładów w rozdziale 4.8.4.1 strona 102).

query = em.createQuery("SELECT COUNT(o) FROM Osoba o");
liczbaOsob = (Long) query.getSingleResult();
assert liczbaOsob == 4 : "4 encje powinny być zwrócone, a otrzymano " + liczbaOsob;

query = em.createQuery("SELECT COUNT(o.dzienImienin) FROM Osoba o");
liczbaOsobPoDzienImienin = (Long) query.getSingleResult();
assert liczbaOsobPoDzienImienin == 2 : "2 encje mają dostępny atrybut dzienImienin, a otrzymano " + liczbaOsobPoDzienImienin;

query = em.createQuery("SELECT COUNT(o) FROM Osoba o WHERE o.dzienImienin IS NOT NULL");
liczbaOsobPoDzienImienin = (Long) query.getSingleResult();
assert liczbaOsobPoDzienImienin == 2 : "2 encje mają dostępny atrybut dzienImienin, a otrzymano " + liczbaOsobPoDzienImienin;

Jak już wspomniałem, rozdział 4.8.4.1 jest zbiorem 5 przykładów z opisem ich wyników. Dobra lektura na utrwalenie wiadomości.