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

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

Podstawy

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

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

SELECT h FROM Hero h

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

SELECT h.name FROM Hero h

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

SELECT h.dragon.name FROM Hero h

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

Filtrowanie wyników i złączenia

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

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

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

SELECT h.name, h.level FROM Hero h 

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

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

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

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

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

SELECT COUNT(h) FROM Hero h

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

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

Jak to uruchomić?

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

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

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

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

Wynikiem działania powyższego kodu jest:

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

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

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

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

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

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

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

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

No dobrze, a mogę dorzucić parametr?

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

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

lub tak:

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

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

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

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

Wykonywanie zapytań – co jeszcze?

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

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

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

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

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

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

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

    // GETTERY I SETTERY POMINIETE
}

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

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

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

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

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

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

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

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

Podpowiedzi

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

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

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

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

Operacje masowe

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

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

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

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

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

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

Oto konsola:

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

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

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

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s