17 marca 2007

Java Persistence - Rozdział 4.4 Klauzula FROM i deklaracje nawigacyjne

...co w oryginale przedstawia się jako 4.4 The FROM Clause and Navigational Declarations.

Klauzula FROM definiuje dziedzinę (obszar działania) zapytania przez deklaracje zmiennych identyfikujących. Zmienna identyfikująca jest identyfikatorem zadeklarowanym w klauzuli FROM zapytania. Dziedzina zapytania może być ograniczona przez wyrażenia ścieżkowe (oraz przez samą ich deklarację bez wykorzystania w klauzuli WHERE - wyjaśnienie znajduje się poniżej w sekcji 4.4.7 Klauzula FROM a SQL).

Zmienne identyfikujące wyznaczają egzemplarze encji.

Przykład: Odszukaj osoby, którzy uczestniczą w projekcie Apache Geronimo.

SELECT o FROM Osoba o JOIN o.projekty p WHERE p.nazwa = 'Apache Geronimo'

Klauzula FROM może zawierać wiele definicji zmiennych identyfikujących oddzielonych przecinkiem.

Przykład: Odszukaj osoby, których numer jest większy niż numer Jacka Laskowskiego.

SELECT DISTINCT o1 FROM Osoba o1, Osoba o2 WHERE o1.numer > o2.numer AND o2.imie = 'Jacek' AND o2.nazwisko = 'Laskowski'

Specyfikacja przedstawia składnię FROM w notacji BNF. Nie zamierzam kopiować składni i zachęcam do zajrzenia do specyfikacji. Poszczególne jej części omówione są poniżej.

4.4.1 Identyfikatory

Identyfikator jest ciągiem znaków o nieograniczonej długości. Identyfikator musi spełniać wymagania identyfikatorów w języku Java (korzysta się z metod Character.isJavaIdentifierStart oraz Character.isJavaIdentifierPart). Znak zapytania jest zarezerwowany dla JPQL (ang. Java Persistence Query Language, co wcześniej nazywałem JPA-QL, więc od teraz będzie krócej).

Następujące słowa są zarezerowanymi identyfikatorami (niektóre jeszcze nie wykorzystywane - zagadka: o jakich mowa?): SELECT, FROM, WHERE, UPDATE, DELETE, JOIN, OUTER, INNER, LEFT, GROUP, BY, HAVING, FETCH, DISTINCT, OBJECT, NULL, TRUE, FALSE, NOT, AND, OR, BETWEEN, LIKE, IN, AS, UNKNOWN, EMPTY, MEMBER, OF, IS, AVG, MAX, MIN, SUM, COUNT, ORDER, BY, ASC, DESC, MOD, UPPER, LOWER, TRIM, POSITION, CHARACTER_LENGTH, CHAR_LENGTH, BIT_LENGTH, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, NEW, EXISTS, ALL, ANY, SOME.

Nieistotna jest wielkość liter w identyfikatorach. Zarezerwowane identyfikatory nie mogą być używane jako zmienne identyfikujące. Zaleca się również niekorzystanie ze słów kluczowych języka SQL, które mogą zostać użyte w przyszłych wersjach specyfikacji.

4.4.2 Zmienne identyfikujące

Zmienna identyfikująca jest poprawnym identyfikatorem zadeklarowanym w klauzuli FROM.

Wszystkie zmienne identyfikujące muszą być zadeklarowane w klauzuli FROM. Zmienne identyfikujące nie mogą być zadeklarowane w innych klauzulach.

Zmienna identyfikująca nie może być zarezerwowanym identyfikatorem lub posiadać nazwę identyczną jak dowolna encja w tej samej PU.

Nieistotna jest wielkość liter w zmiennych identyfikujących.

Przykład: Zastosowanie różnych wielkości liter w zmiennej identyfikującej os.

SELECT DISTINCT oS FROM Osoba os, Osoba o2 WHERE OS.numer > o2.numer AND o2.imie = 'Jacek' AND o2.nazwisko = 'Laskowski'

Zmienna identyfikująca zawsze wyznacza referencję do pojedyńczej wartości. Zmienna identyfikująca może być zdefiniowana w jeden z 3 sposobów:
  • deklarację AS
  • klauzulę JOIN
  • w deklaracji kolekcji
Deklaracje zmiennych identyfikujących są wyliczane od lewej do prawej i deklaracja może używać wyniku poprzednich deklaracji zmiennych w zapytaniu.

4.4.3 Deklaracja AS zmiennych identyfikujących

Składnia deklaracji AS zmiennych identyfikujących jest podobna do składni SQL. Słowo AS jest opcjonalne.

deklaracja_zmiennej_AS ::= nazwa_abstrakcyjnego_schematu [AS] zmienna_identyfikująca

Deklaracje AS pozwalają na określenie punktu początkowego dla obiektów niedostępnych poprzez relacje.

Jeśli chcielibyśmy skorzystać z możliwości porównywania egzemplarzy encji z samą sobą, konieczne jest zdefiniowanie więcej niż jednej zmiennej identyfikującej wskazującą na tę samą encję.

Przykład: Użycie dwóch różnych zmiennych identyfikujących - o1 i o2 - o AST tej samej encji - Osoba.

SELECT DISTINCT o1 FROM Osoba o1, Osoba o2 WHERE o1.numer > o2.numer AND o2.imie = 'Jacek' AND o2.nazwisko = 'Laskowski'

4.4.4. Wyrażenia ścieżkowe (ang. path expression)

Zmienna identyfikująca, po której następuje kropka '.' będącym symbolem nawigacyjnym, a po niej pole-stanu lub pole-relacji jest wyrażeniem ścieżkowym. Typ wyrażenia ścieżkowego jest typem wyznaczonym jako wynik przejścia po ścieżce, tj. typem pola-stanu lub pola-relacji.

Wyrażenia ścieżkowe mogą składać się z wielu kropek będącymi separatorami kolejnych pól-relacji, które pozwalały na dalsze przejścia w grafie powiązań encji, jeśli poszczególne części nie są kolekcjami encji.

Przejścia po wyrażeniach ścieżkowych określane są poprzez semantykę inner join, tj. jeśli wartość niekończącego pola-relacji w wyrażeniu ścieżkowym jest null, przyjmuje się, że ścieżka nie ma wartości i nie uczestniczy w wyznaczaniu wyniku. Innymi słowy, w wyrażeniu ścieżkowym order.customer.imie, jeśli customer jest null, wtedy wartość nie będzie brała udziału w wykonaniu zapytania.

Specyfikacja przedstawia składnię wyrażeń ścieżkowych pojedyńczej wartości (nie-kolekcji) oraz wartości kolekcyjnych.

Nawigacja (przejście) do związanej encji wyznacza wartość wyrażenia jako AST tej encji.

Typ wyrażenia ścieżkowego kończącego się na polu-stanu wyznacza wartość wyrażenia jako AST pola encji, wskazanego przez pole-stanu.

Niedozwolone jest konstruowanie wyrażenia ścieżkowego z części ścieżki, która jest kolekcją. Specyfikacja przychodzi z pomocą w wytłumaczeniu tej kwestii i tak, jeśli Order o i o.lineItems jest polem-relacji do kolekcji pozycji na zamówieniu (ang. line items), wtedy ścieżka postaci o.lineItems.product jest niedozwolona. W takim przypadku należy wprowadzić zmienną identyfikującą dla kolekcji tak, aby inne wyrażenia ścieżkowe mogły przejść po wszystkich elementach kolekcji.

Przykład wykorzystania zmiennej identyfikującej kolekcję w klauzuli FROM

SELECT DISTINCT o FROM Osoba AS o, IN(o.projekty) p WHERE p.nazwa = 'Apache Geronimo'

Słowo kluczowe AS w klauzuli FROM jest opcjonalne w powyższym zapytaniu.

4.4.5 Łączenia (ang. joins)

Zanim przedstawię informacje zawarte w tej części specyfikacji, wiele z wyjaśnianych tutaj pojęć jest opisanych na wazniak.mimuw.edu.pl (aka ważniak).

Wewnętrzne łączenie (ang. inner join) może być zdefiniowane wprost przez użycie iloczynu kartezjańskiego w klauzuli FROM oraz warunku łączenia w klauzuli WHERE. W przypadku braku warunku łączenia, sprowadza się to do produktu kartezjańskiego.

Główne użycie dla tego typu łączenia jest w przypadku braku wykorzystania relacji poprzez klucz obcy, który jest związany z relacją encji.

Przykład zapożyczony ze specyfikacji:

select c from Customer c, Employee e where c.hatsize = e.shoesize

Jak zaznaczono w specyfikacji, użycie tego typu wiązań wewnętrznych (nazywanych również theta-łączeniami, ang. theta-join) jest mniej wykorzystywana ze względu na częstsze stosowanie łączeń poprzez relacje encji wprost zdefiniowanych w zapytaniu.

Składania (wyrażonych dosłownie) operacji łączenia korzysta ze słów kluczowych FETCH, LEFT [OUTER] JOIN i INNER JOIN.

Kolejne sekcje omawiają wspierane typy operacji łączenia INNER i OUTER.

4.4.5.1 Wewnętrzne łączenia - INNER JOIN

Składnia operacji wewnętrznego łączenia jest następująca:

[ INNER ] JOIN wyrażenie_ścieżkowe_łączenia [AS] zmienna_identyfikująca


Przykład: Odszukaj osoby, które należą do przynajmniej jednego projektu i mieszkają w Polsce

SELECT o FROM Osoba o JOIN o.projekty p WHERE o.kraj = 'Polska'

, które jest równoważne z zapytaniem, które korzysta ze słowa kluczowego INNER:

SELECT o FROM Osoba o INNER JOIN o.projekty p WHERE o.kraj = 'Polska'

, a to z kolei jest równoznaczne z zapytaniem korzystającym z konstrukcji IN:

SELECT o FROM Osoba o, IN(o.projekty) p WHERE o.kraj = 'Polska'

4.4.5.2 Lewostronne łączenia zewnętrzne - LEFT OUTER JOIN

LEFT JOIN oraz LEFT OUTER JOIN są synonimami. Jak można wyczytać na ważniaku (przez co materiał specyfikacji JPA staje się znacznie prostszy) łączenia zewnętrzne pozwalają na pobranie encji, gdzie wartości spełniające warunek łączenia mogą nie istnieć. W takim przypadku odpowiadające wartości w encji są null.

Składnia operacji lewostronnego łączenia zewnętrznego jest następująca:

LEFT [OUTER] JOIN wyrażenie_ścieżkowe_łączenia [AS] zmienna_identyfikująca

Przykład: Pobranie wszystkich osób z Polski bez względu na ich przynależność do projektów, tj. jeśli osoba nie uczestniczy w projekcie (lista projektów wynosi null) to i tak zostanie zawarta w wyniku zapytania.

SELECT DISTINCT o FROM Osoba o LEFT JOIN o.projekty p WHERE o.kraj = 'Polska'

Jest to równoważne z zapytaniem korzystającym ze słowa kluczowego OUTER:

SELECT DISTINCT o FROM Osoba o LEFT OUTER JOIN o.projekty p WHERE o.kraj = 'Polska'

Może wydawać się, że wystarczyłoby wydać zapytanie

SELECT DISTINCT o FROM Osoba o WHERE o.kraj = 'Polska'

i uzyskać żadane dane. Zakładamy niewprost, że Osoba jest w relacji z encją Projekt, więc tam, gdzie potrzebne dane będą automatycznie pobrane przez zarządcę trwałości. Pamiętajmy jednak, że istnieje możliwość utworzenia takiego modelu, gdzie Osoba i Projekt nie będą w relacji, a w/w zapytanie będzie mapowane przy użyciu adnotacji @SqlResultSetMapping. W takim przypadku możemy zażyczyć sobie pobrania informacji dodatkowych poza tymi koniecznymi dla encji Osoba.

Istotnym przypadkiem użycia LEFT JOIN jest wczesne (ang. eager) pobranie wartości z prawej strony łączenia w zapytaniu (w powyższym przypadku danych o projektach) w połączeniu z pobierającego łączenia - JOIN FETCH.

4.4.5.3 Pobierające łączenia - JOIN FETCH

Pobierające łączenie (ang. fetch join) umożliwia wczesne pobranie wartości asocjacji jako rezultat poboczny wykonania zapytania. Łączenie pobierające jest zdefiniowane nad encją i jej powiązanych encjami.

Patrząc na powyższy przykład z encjami Osoba i Projekt, gdyby nie zastosować JOIN FETCH, domyślnie informacje o projektach są pobieranie z opóźnieniem, co nie zawsze jest wskazane, np. w przypadku aplikacji internetowych, gdzie pobieramy encje odłączając je (dla entuzjastów Hibernate będzie odłączenie od egzemplarza Session podczas, gdy w JPA będzie to zarządca trwałości, czyli egzemplarz EntityManager).

Składnia operacji pobierającego łączenia jest następująca:

łączenie_pobierające ::= [ LEFT [OUTER] | INNER ] JOIN FETCH wyrażenie_ścieżkowe_łączenia

Asocjacja, która wskazywana jest przez prawą stronę klauzuli JOIN FETCH musi być asocjacją, która należy do encji i jest zwracana jako wynik zapytania. Niedozwolone jest, aby skorzystać ze zmiennej identyfikującej encje wskazywane przez prawą stronę klauzuli FETCH JOIN, a zatem odwołania do wprost pobieranych encji nie mogą wystąpić gdziekolwiek w zapytaniu.

Przykład: Pobranie osób mieszkających w Polsce z tym, że informacje o projektach będą również pobrane (wczesne pobranie).

SELECT DISTINCT o FROM Osoba o LEFT JOIN FETCH o.projekty WHERE o.kraj = 'Polska'

Łączenie pobierające ma tę samą semantykę łączenia jak łączenia wewnętrzne i zewnętrzne, jednakże powiązane obiekty wskazane po prawej stronie operacji łączenia pobierającego nie są zwracane jako wynik zapytania lub w inny sposób uczestniczą w zapytaniu. Stąd, jeśli osoba należy do 3 projektów, to powyższe zapytanie zwróci 3 encje Osoba, które de facto będą identyczne (i to stwierdzenie pozostawia mnie w stanie błogiego zastanowienia i niezrozumienia tematu, po co w takim razie chciałbym wykonywać takie zapytanie?).

4.4.6 Deklaracje kolekcji składowych

Zmienna identyfikująca może operować na wartościach kolekcji, które są pobrane w wyniku wyrażenia ścieżkowego. Takie wyrażenie ścieżkowe jest ścieżką korzystającą z pól-relacji encji. Wyrażenie ścieżkowe może opierać się o inne wyrażenie ścieżkowe, które również korzysta z pól-relacji odpowiednich encji.

Zmienna identyfikująca deklaracji kolekcji składowej jest definiowana używając operatora IN (słowo kluczowe). Parametrem dla IN jest wyrażenie ścieżkowe będące kolekcją.

Składnia deklaracji zmiennej identyfikującej kolekcji składowej jest następująca:

deklaracja_kolekcji_składowej ::= IN (wyrażenie_ścieżkowe_będące_kolekcją) [AS] zmienna_identyfikująca

Korzystając z przykładu opartego o zamówienie (Order), pozycje na zamówieniu (LineItem) oraz produkty (Product) w specyfikacji, poniższe zapytanie:

SELECT DISTINCT o FROM Order o JOIN o.lineItems l JOIN l.product p WHERE p.productType = ‘office_supplies’

może zostać równoważnie zapisane korzystając z operatora IN:

SELECT DISTINCT o FROM Order o, IN(o.lineItems) l WHERE l.product.productType = ‘office_supplies’

Szczegółowe omówienie zapytania znajduje się w specyfikacji na stronie 87.

4.4.7 Klauzula FROM a SQL

JPQL traktuje klauzulę FROM podobnie jak SQL, tj. zadeklarowane zmienne identyfikujące wpływają na wynik zapytania, nawet jeśli zmienne nie są wykorzystane w klauzuli WHERE. Klauzula FROM ustanawia dziedzinę zapytania, więc twórca zapytania powinien uważnie definiować zmienne identyfikujące, gdyż dziedzina zapytania zależy od istnienia wartości zadeklarowanego typu.

Przykładowo w specyfikacji zaprezentowano zapytanie

SELECT o FROM Order AS o, IN(o.lineItems) l, Product p

, które definiuje 3 zmienne identyfikujące - o, l, p. Zapytanie zwraca wszystkie zamówienia, które posiadają przynajmniej jedną pozycję i istniejący produkt. Jeśli nie istnieje jakikolwiek produkt w bazie, dziedzina zapytania będzie pusta i żadne z zamówień nie zostanie zwrócone w wyniku zapytania (!)

4.4.8 Polimorfizm

Zapytania w JPQL są polimorficzne. Klauzula FROM zapytania wyznacza nie tylko egzemplarze wymienionej w zapytaniu klasy encji, ale również wszystkich jej klas pochodnych (podklas). Egzemplarze zwrócone w wyniku zapytania zawierają egzemplarze klas pochodnych, które spełniają warunek zapytania (co nie było możliwe w EJB 2.1, gdzie nie istniało pojęcie dziedziczenia encji).

Tym samym specyfikacja kończy omawianie klauzuli FROM i przechodzi do klauzuli WHERE (rozdział 4.5), o czym będę relacjonował w kolejnym wpisie. Za mną klauzula FROM i w końcu udało mi się doczytać o wszystkich łączeniach w JPQL. Zabieram się do dalszego modelowania mojej przykładowej aplikacji z osobami, projektami i rolami (będącymi typem wyliczeniowym - enum). Najwyższa pora na spanie, ale wcześniej zajrzę do książki Hibernate. Od nowicjusza do profesjonalisty, gdzie zamierzam doczytać szczegóły. Za każdym razem, kiedy potrzebuję dobrego przewodnika po Hibernate (i po części JPA) zaglądam do niej i nie ukrywam, że warto ją mieć na swojej półce. Mało mam książek informatycznych, ale ta wciąż leży pod ręką.