Dajcie kawałek SQLa – zapytania criteria API

W poprzednim wpisie zajmowaliśmy się JPQL. Możliwość tworzenia zapytań podobnych składniowo do SQL, ale z użyciem notacji obiektowej z całą pewnością upraszcza programowanie. Dzisiaj omówię rozwiązanie programistyczne – czyli tworzenie zapytań za pomocą notacji stricte obiektowej z użyciem odpowiednich metod – criteria queries.

Podstawy

Standardowo rozpoczniemy najprostszym możliwym przykładem. Oto wyjściowe zapytanie JPQL:


String jpql = "SELECT h.name FROM Hero h WHERE h.id = :id";
Query query = entityManager.createQuery(jpql, String.class)
                           .setParameter("id",1L);

System.out.println("Imie bohatera: " + query.getSingleResult()  );

Oczywiście jest to dość proste zapytanie, które jednakże posiada dość istotną wadę. Otóż o ile możemy użyć w nim parametrów, o tyle (np. w przypadku NamedQuery) musimy je określić przed uruchomieniem programu. Co w sytuacji kiedy wykonywane zapytanie zależy od danych dostarczonych przez użytkownika aplikacji? Oczywiście możemy sklejać zapytania JPQL bazując na przekazanych danych, ale ani to czytelne, a i o błąd dość łatwo. Właśnie dla takich sytuacji jak najbardziej przydają się Criteria Queries.

By rozpocząć od kodu – oto powyższe zapytanie przepisane z użyciem kryteriów:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);

Root<Hero> hh = query.from(Hero.class);
query.select(hh)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);
System.out.println("Imie bohatera: " + tq.getSingleResult().getName()  );

Kodu jakby więcej, ale jest on o wiele bardziej elastyczny. Podstawowym elementem wykorzystywania kryteriów jest interfejs CriteriaBuilder pobierany z instancji Entity Managera. Jest to punkt wejścia dla używania kryteriów. Dzięki metodzie tego interfejsu o nazwie createQuery tworzymy obiekt typu CriteriaQuery. To ten obiekt będzie reprezentował wszystkie warunki, które zamierzamy użyć w zapytaniu. Troszeczkę nie do końca oczywiste jest istnienie obiektu typu Root. Interfejs Root jest odpowiednikiem zmiennej wskaźnikowej w zapytaniach JPQL. Czyli jeśli w zapytaniu JPQL zastosujemy taką konstrukcję:


SELECT h FROM Hero h

To dokładnym odpowiednikiem tej konstrukcji w kryteriach jest właśnie:


Root<Hero> hh = query.from(Hero.class);

Na chwilę obecną wystarczy takie opisanie tego interfejsu. Dalej w kodzie odwołujemy się do obiektu reprezentującego nasze criteria query używając metod tego interfejsu jak i obiektu CriteriaBuilder. Nazwy metod są dość czytelne i póki co nie ma potrzeby ich wyjaśniać. Następnie wykorzystując nasz obiekt kryteriów tworzymy znaną już nam instancję TypedQuery i wywołujemy metody pobierające wyniki. Dlaczego w ten sposób? Przypomnijmy sobie, że w przypadku JPQL schemat działania jest taki sam, z tym tylko, że jako parametr przy tworzeniu obiektu zapytania służy łańcuch tekstowy z zapytaniem, a nie obiekt. Zmniejsza to powiązania między obiektami – obiekt kryteriów niesie informację co ma być pobrane z bazy, zaś obiekt TypedQuery wie w jaki sposób wykonać owo zapytanie. W tym miejscu warto jeszcze dodać, iż powyższy kod może zostać zapisany bez używania generyków, jednakże wówczas nie mamy możliwości sprawdzenia poprawności niektórych wywołań na etapie kompilacji. Ja preferuję używanie generyków gdzie to tylko możliwe.

Podstawy tworzenia zapytań

Jak widzieliśmy w powyższym przykładzie cała zabawa z kryteriami rozpoczyna się od interfejsu CriteriaBuilder. Posiada on trzy metody, których możemy użyć do stworzenia instancji reprezentującej interfejs CriteriaQuery:

  • createQuery(Class) – tworzy instancję generyczną, najczęściej używana, co zresztą zademonstrowałem na poprzednim listingu
  • createQuery() – to samo co powyżej, ale w wersji zwracającej Object
  • createTupleQuery() – metoda będąca tak naprawdę wywołaniem metody createQuery(Tuple.class). O tym czym jest Tuple (krotka) napiszę później

Kiedy posiadamy już obiekt CriteriaQuery możemy przystąpić do definiowania naszego zapytania. Podstawowe elementy języka SQL oraz tym samym JPQL jak słowa kluczowe SELECT, WHERE czy FROM mają swoje odpowiedniki w postaci metod obiektu CriteriaQuery. Jednakże zanim do nich przejdziemy zajmiemy się dość istotnym elementem – rdzeniem (root) zapytania. W przypadku JPQL mieliśmy do czynienia ze zmienną aliasu. Dzięki niej mogliśmy odwoływać się do kolejnych wartości encji, na której pracowaliśmy. W przypadku kryteriów również musimy stworzyć uchwyt, za pomocą którego będziemy mogli ‘dostać się’ do danych. W poprzednim przykładzie służyła do tego następująca konstrukcja:


Root<Hero> hh = query.from(Hero.class);

 

Posługujemy się tutaj interfejsem Root – zachęcam do zapoznania się z rodzicami tego interfejsu. Dzięki temu dość łatwo zrozumieć, co jest wykonalne za pomocą tegoż interfejsu (wrócę do tego). Na razie trzeba zaznaczyć tylko, iż w zapytaniu możemy użyć kilkunastu tego typu obiektów (co dotyczy zwłaszcza złączeń). Drugim elementem, który można wyróżnić na początku poznawania kryteriów są path expressions. W skrócie – jest to ‘ścieżka’, za pomocą której dochodzimy do interesujących nas własności obiektu. W poniższym zapytaniu:


SELECT h FROM Hero WHERE h.id = :id

 

Tego typu wyrażeniem jest fragment h.id. Przekładając powyższe zapytanie na kryteria otrzymamy kod, który przedstawiłem na samym początku:


Root<Hero> hh = query.from(Hero.class);
query.select(hh)
     .where(cb.equal(hh.get("id"), 1L));

 

Fragment hh.get(“id”) to odpowiednik kropki w zapytaniach JPQL.

 

Klauzula SELECT

 

Standardowo do pobierania wyniku zapytania służy metoda select. Przyjmuje ona jako argument obiekt implementujący interfejs Selection. Do tej pory przekazywaliśmy do tej metody instancję o typie Root – w ten sposób informowaliśmy JPA, że interesuje nas encja jako wynik zapytania. Jednakże jeśli interesuje nas np. samo imię maga (czyli wartość łańcuchowa), wówczas musimy zastosować następującą konstrukcję:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<Hero> hh = query.from(Hero.class);
query.select(hh.<String>get("name"))
     .where(cb.equal(hh.get("id"), 1L));
TypedQuery<String> tq = entityManager.createQuery(query);

System.out.println("Imie bohatera: " + tq.getSingleResult()  );

Argument przekazywany do metody select musi być kompatybilny z typem zwracanym przez definicję zapytania (CriteriaQuery). Dostawca JPA nie jest w stanie po samej nazwie wyciąganego parametru rozpoznać jego typu, dlatego też musimy użyć parametryzacji.

Nadszedł czas by opisać wspomniane już krotki. Co to są krotki (tuple)? Krotka to w skrócie obiekt, który zawiera dane otrzymane z zapytania (reprezentuje wiersz danych). Dostęp do nich jest możliwy zarówno w notacji indeksowej (czyli podajemy po prostu indeks zwróconego wyniku jak w czystym JDBC), albo za pomocą nazwy kolumn (jeśli zaliasujemy je). Dzięki temu jeżeli potrzebujemy tylko wyciągnąć z bazy konkretne dane, bez narzutu tworzenia encji wówczas krotki są rozwiązaniem – łatwo się po nich iteruje, a do tego są wydajniejsze niż encje. Krótki kawałek kodu:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<Hero> hh = query.from(Hero.class);
query.select(cb.tuple( hh.get("id").alias("id"), hh.get("name").alias("name")  ) )
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Tuple> tq = entityManager.createQuery(query);

for (Tuple t : tq.getResultList()) {
   System.out.println(t.get("id") + " " + t.get(1)  );
}

Skąd krotki? Ano bo musimy znać je by w pełni omówić metodę multiselect interfejsu CriteriaQuery. Jak widać z ostatniego kawałka kodu stworzenie odpowiednich wyrażeń dla metody select jest trochę ‘długaśne’. Co więcej – jeżeli chcemy wyciągnąć kilkanaście różnych danych (bardzo często z różnych tabel), zapytanie potrafi się z lekka rozrosnąć. Istotne jest również, że przy wyciąganiu szeregu kolumn rezultat może mieć kilka postaci – obiektu anonimowej klasy tworzonej w miejscu użycia, krotki lub tablicy obiektów (w zależności od typu używamy w metodzie select przy tworzeniu argumentu jedną z metod – tuple, construct lub array). By oszczędzić pracy programiście mamy dostęp do metody multiselect, która po prostu przyjmuje listę wartości, które chcemy wyciągnąć.
Tym samym nasz poprzedni przykład z krotkami dałoby się zapisać następująco:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<Hero> hh = query.from(Hero.class);
query.multiselect( hh.get("id").alias("id"), hh.get("name").alias("name") )
     .where(cb.equal(hh.get("id"), 1L))

//Reszta kodu po staremu

 

Klauzula FROM

 

O metodzie from już wspomniałem, pokazałem także przykład kodu. Teraz zajmiemy się czymś bardziej skomplikowanym, a konkretniej – złączeniami. Do ich obsługi używamy metodę o nazwie (a jakże) – join. Jak zawsze sugeruję zajrzeć do dokumentacji interfejsu Join – wtedy też dość łatwo zrozumiemy dlaczego nie ma problemu z łączeniem wywołań tej metody. Dowiemy się również wówczas, że metoda ta zwraca obiekt sparametryzowany – gdzie musimy zamieścić informację o typie encji, która jest podstawą złączenia, a także encji docelowej. Zobaczmy na przykładzie:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Join<Hero,String> nicknamesJoin = hh.join("nicknames");
query.select(hh.alias("hero"))
     .distinct(true)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);

for (Hero hero : tq.getResultList()) {
     System.out.println("Id bohatera: " + hero.getId()  );
     for( Nickname ne : hero.getNicknames() ) {
            System.out.println("Nickname: " + ne.getNick() );
     }
}

Pisząc o złączeniach nie sposób też poruszyć dość fajnej możliwości jaką są fetch join, czyli zapytanie, które przy okazji wykonania (jako efekt uboczny) pobiera do persistence context encje nie będące bezpośrednio używane podczas dostępu do zbioru wynikowego. Oto przykład:


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
hh.fetch("nicknames");
query.select(hh.alias("hero"))
     .distinct(true)
     .where(cb.equal(hh.get("id"), 1L));

TypedQuery<Hero> tq = entityManager.createQuery(query);
Hero backupHero = null;

for (Hero hero : tq.getResultList()) {
     System.out.println("Id bohatera: " + hero.getId()  );
     backupHero = hero;
}                               

for( Nickname n : backupHero.getNicknames() ) {
    System.out.println(n.getNick());
}   

Druga pętla for jest tylko dla celów demonstracyjnych – prawda jest taka, że bez niej oryginalne zapytanie też wyciąga już informacje o przydomkach i zapisuje je w persistence context.

 

Klauzula WHERE

 

Klauzula WHERE jest w kryteriach dostępna poprzez metodę where(). Pochodzi ona z interfejsu AbstractQuery i jak zawsze zachęcam do zapoznania się z dokumentacją. Można wywoływać tę metodę bezargumentowo, albo też z parametrami w postaci obiektów Predicate lub pojedynczego argumentu Expression. Należy pamiętać, że każde kolejne wywołanie metody where() powoduje nadpisanie uprzednio dodanych warunków!

By odpowiednio uformować klauzulę WHERE dostępnych jest wiele metod – generalnie nie ma sensu ich tu listować skoro zrobiło to już za mnie Oracle. W dalszej części wpisu zajmę się tylko zagadnieniami wymagającymi trochę więcej uwagi – zaczniemy od predykatów.

W przypadku używania metody where mamy możliwość użycia kilku argumentów typu Predicate (np. w formie tablicy stworzonej z listy, gdyż metoda przyjmuje jako parametr varargs ). Co jednak mamy logikę warunkową i wolelibyśmy mieć jeden obiekt do przekazania? Da się to załatwić za pomocą metody conjuction() interfejsu CriteriaBuilder


CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Predicate criteria = cb.conjunction();
//
// Warunki oczywiscie sa przykladowe i sluza tylko ilustracji
//
if( true ) {
     ParameterExpression<Long> id = cb.parameter(Long.class, "id");
     criteria = cb.and(criteria, cb.equal(hh.get("id"), id ) );
}
if( true ) {
     ParameterExpression<String> imieMaga = cb.parameter(String.class, "name");
     criteria = cb.and(criteria, cb.like( hh.<String>get("name"), imieMaga ) );
}                                   

query.select(hh)
     .where(criteria);

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("id", 1L);
tq.setParameter("name", "Chlebik%");                             

for (Hero hero : tq.getResultList()) {
    System.out.println("Id bohatera: " + hero.getId()  );
}

W powyższym przykładzie mieliśmy też okazję skorzystać z parametrów. W przypadku criteria API istnieje też możliwość używania literałów (większość metod przyjmuje albo Expression albo literały). Gdzie jednakże nie jest to możliwe, wówczas można literał po prostu zawrapować za pomocą metody literal() – literał dla wartości NULL tworzymy za pomocą metody nullLiteral( Class clazz ). Wróćmy jednak do parametrów – w powyższym przykładzie zastosowaliśmy trochę dłuższą drogę. Stworzyliśmy obiekt ParameterExpression by dodać go potem do zapytania. Jeśli jednakże ta konkretna definicja parametru nie będzie już w kodzie używana, wówczas możemy zrobić to trochę bardziej elegancko:


query.select(hh)
     .where(cb.like(hh.<String>get("name"), cb.parameter(String.class, "name") ) );

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("name", "Chlebik%");                             

Oczywiście mamy też możliwość budowania podzapytań. Da się to osiągnąć poprzez metodę subquery() interfejsu AbstractQuery, zwracana przez tę metodę wartość to instancja Subquery, która zasadniczo niewiele różni się od CriteriaQuery. Dość gadania, napiszmy jakiś kod (tak, wiem, że przykład jest przekombinowany, ale taki mamy model danych 😉 )

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);

Subquery<Hero> heroesWithWeapons = query.subquery(Hero.class);
Root<Hero> heroesWithWeaponsRoot = heroesWithWeapons.from(Hero.class);
Join<Hero,Weapon> heroesWithWeaponsJoin = heroesWithWeaponsRoot.join("weapons");
heroesWithWeapons.select( heroesWithWeaponsRoot ).where( cb.equal(
                                                            heroesWithWeaponsJoin.get("name"),
                                                            cb.parameter( String.class, "weaponName" ) )
);

// Radze zwrocic uwage na konstrukcje uzyta w metodzie IN()
query.select(hh)
     .where( cb.in( hh ).value(heroesWithWeapons) );

TypedQuery<Hero> tq = entityManager.createQuery(query);
tq.setParameter("weaponName", "Super-Duper-Miecz");                             

Klauzule sortujące i grupujące

Do tej pory skupialiśmy się na pobieraniu danych i warunkach tegoż pobierania. Teraz przyszła pora na to,co zazwyczaj znajduje się po klauzuli WHERE – czyli na instrukcje grupujące i sortujące. Sortowanie jako prostsze omówię na początek – służy do tego obiekt typu Order, który można otrzymać używając metod asc() lub desc() interfejsu CriteriaBuilder.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);

query.select(hh).orderBy( cb.desc(hh.get("level")) ) ;

Metody agregujące i grupujące są równie proste w użyciu – na tym przykładzie wybieramy bohaterów, którzy mają przynajmniej jedno bóstwo:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Hero> query = cb.createQuery(Hero.class);
Root<Hero> hh = query.from(Hero.class);
Join<Hero,Deity> deities = hh.join("deities");

query.select(hh)
     .groupBy( hh )
     .having( cb.ge( cb.count(deities), 1 ) );

TypedQuery<Hero> tq = entityManager.createQuery(query);

To tyle na dziś. W następnej notce napiszę coś więcej na temat silnie typowanych zapytań oraz metadanych.

Advertisements

2 thoughts on “Dajcie kawałek SQLa – zapytania criteria API

  1. Mariusz z Wrocławia

    Znalazłem mały błąd:
    zamiast
    SELECT h FROM Hero
    powinno być
    SELECT h FROM Hero h
    Ponieważ bez tego “h” na końcu JPQL wyrzuci błąd 🙂

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