Prikaži sadržaj

Programiranje baza podataka

12. Napredno kreiranje upita pomoću JPA Criteria API

Ova stranica je pod konstrukcijom!
Ako pronađete grešku ili propust, molimo Vas da nam skrenete pažnju otvaranjem primedbe na zvaničnom GitHub repozitorijumu.

U prethodnom poglavlju smo diskutovali o korišćenju jezika HQL, specifičnog za potrebe aplikacija sa Hibernate podrškom, za izdvajanje skupova podataka iz relacione baze podataka. Videli smo da se HQL konceptualno zasniva za jeziku SQL, sa razlikom da, umesto baratanja tabelama u relacionoj bazi podataka, sve restrikcije, projekcije, spajanja itd. koriste Java klase za postavljanje ograničenja koja rezultati upita moraju da ispunjavaju.

Sličan koncept, ali nešto opštiji u smislu upotrebe, predstavlja JPA Criteria API. Ideja je da se umesto navođenja imena pod niskom (što je bio slučaj u HQL upitima), koriste napredniji koncepti za konstrukciju dinamičkih upita. Prednost ovog načina korišćenja jeste upravo u tome što se umesto korišćenja niski za zadavanje upita, koriste inteligentne programerske tehnike, kao što je ulančavanje metoda. Dodatno, koriste se prednosti programskog jezika Java.

12.1 Kratak pregled JPA Criteria API

Započnimo ovo poglavlje davanjem primera sa ciljem da ilustrujemo promenu u razmišljanju između konstrukcije HQL upita i korišćenja JPA Criteria API. Naredni HQL upit izdvaja sve zaposlene u kompaniji čije je ime “John Smith”:

String hql = "FROM Employee e WHERE e.name = \"John Smith\"";
org.hibernate.query.Query<Employee> upit = 
    session.createQuery(hql, Employee.class);

Ekvivalentan rezultat koji se dobija korišćenjem JPA Criteria API je:

CriteriaBuilder cb          = session.getCriteriaBuilder();
CriteriaQuery<Employee> c   = cb.createQuery(Employee.class);

Root<Employee> emp = c.from(Employee.class);
c.select(emp)
 .where(cb.equal(emp.get("name"), "John Smith"));

Očigledno, ovi pristupi se jasno razlikuju. HQL upit se konstruiše na isti način kao i SQL upit - navođenjem odgovarajuće sintakse u cilju konstrukcije niske koja sadrži sve informacije. Za razliku od toga, pristup koji se oslanja na JPA Criteria API koristi metode i refleksiju za konstrukciju uslova koje rezultati upita moraju da zadovolje. Kroz ovo poglavlje ćemo se detaljno upoznati sa ovim konceptima.

Radi kompletnosti, objasnimo nešto detaljnije prethodni kod. Za početak primećujemo da se kreira instanca interfejsa CriteriaBuilder tako što se nad objektom klase Session poziva metod getCriteriaBuilder(). Instanca cb interfejsa CriteriaBuilder sadrži veliki broj metoda koji se koriste za definisanje ograničenja rezultata upita. Na primer, pozivom metoda createQuery nad objektom cb kreira jedan novi objekat klase CriteriaQuery koji reprezentuje kostur JPA Criteria API upita. Drugi primer upotrebe klase CriteriaBuilder jeste u kreiranju jednakosnog ograničenja koji odgovara izrazu e.name = \"John Smith\" u HQL upitu, pozivom metoda equal().

Kada imamo kostur novog upita, ostatak koda je manje-više pravolinijski. Potrebno je ustanoviti koren upita, odnosno, klasu od koje se započinje zadavanje ograničenja - u našem primeru to je klasa Employee - što se izvršava pozivanjem metoda from() nad upitom i prosleđivanjem klase koja predstavlja koren. Ovo je ekvivalentno deklarisanju alijasa e u HQL upitu i objekat klase Root predstavlja osnovu za izgradnju ostatka upita. Naredni korak jeste izvršavanje projekcije pozivom metoda select() nad upitom i prosleđivanjem konstruisanog korena upita. Konačno, potrebno je zadati odgovarajuće restrikcije nad upitom, što se izvodi ulančavanjem metoda where() nad projekcijom upita i navođenjem ograničenja restrikcije. S obzirom da se vrši restrikcija nad poljem name klase Employee, sve što je potrebno uraditi jeste pronaći to ime pozivom metoda get() nad korenom upita.

Iako smo napisali više koda u odnosu na pristup zasnovan na HQL jeziku, primetimo sledeće. Ukoliko bismo želeli da dinamički postavljamo uslove restrikcije u upitu, u pristupu koji koristi HQL jezik morali bismo da upravljamo niskama za konstruisanje upita, na primer:

String hql = "FROM Employee e WHERE ";
if (nameRestriction) {
    hql += "e.name = \"John Smith\"";
}
else if (salaryRestriction) {
    hql += "e.salary = 20000";
}
else {
    // ...
}

org.hibernate.query.Query<Employee> upit = 
    session.createQuery(hql, Employee.class);

Ovakav pristup za kreiranje dinamičkih upita ima veliki broj problema. Umesto toga, mnogo je prirodnije osloniti se na mogućnosti samog programskog jezika u kojem se vrši razvoj aplikacije, što nam JPA Criteria API omogućava:

CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Employee> c = cb.createQuery(Employee.class);
Root<Employee> emp = c.from(Employee.class);

String attribute;
String restriction;
if (nameRestriction) {
    attribute = "name";
    restriction = "John Smith";
}
else if (salaryRestriction) {
    attribute = "salary";
    restriction = "20000";
}
else {
    // ...
}

c.select(emp)
 .where(cb.equal(emp.get(attribute), restriction));

Na ovaj način ne moramo da vodimo računa da li smo ispravno konstruisali niske od kojih upit zavisi zato što se način izdvajanja rezultata u poslednjoj liniji nikad ne menja!

12.2 Konstrukcija JPA Criteria API upita

Kao što smo demonstrirali u prethodnoj sekciji, srž JPA Criteria API-ja je u interfejsu CriteriaBuilder, koji se dobija pozivom metoda getCriteriaBuilder() nad objektom sesije. Ovaj interfejs je poprilično širok i služi u razne svrhe u okviru JPA Criteria API-ja. Cilj ovog poglavlja jeste da se upoznamo sa njim.

Interfejs CriteriaBuilder nudi tri metoda za kreiranje novog upita, u zavisnosti od željenog tipa rezultata upita:

  1. Metod createQuery(Class<T>) prihvata klasu koja odgovara rezultatu upita.
  2. Metod createQuery(), bez parametara, odgovara rezultatu tipa klase Object.
  3. Metod createTupleQuery() koristi se za projekcije ili u slučaju kada SELECT klauza sadrži više od jednog izraza i želimo da radimo sa rezultatom u strogo tipiziranom maniru. Zapravo, taj metod predstavlja skraćenicu za poziv prvog metoda createQuery(Tuple.class). Primetimo da je Tuple interfejs koji sadrži različite objekte ili podatke i primenuje tipiziranost nad agregatnim delovima. Može biti korišćen kada je rezultat upita više podataka, pri čemu želimo da ih kombinujemo u jedinstveni tipizirani objekat.

JPA Criteria API se sastoji od velikog broja interfejsa koji rade zajedno u cilju modeliranja strukture JPA upita. Kako budemo prolazili kroz ovo poglavlje, biće korisno pratiti dijagram na narednoj slici i posmatrati odnose između interfejsa.

Interfejsi u JPA Criteria API

12.3 Osnovna struktura

U prethodnom poglavlju smo videli da u jeziku HQL možemo koristiti narednih šest klauza za konstrukciju klasičnih SELECT upita: SELECT, FROM, WHERE, ORDER BY, GROUP BY and HAVING. Svaka od ovih HQL klauza ima ekvivalentan metod nad nekim od JPA Criteria API interfejsa. Naredna tabela sumira ove metode.

HQL klauza Interfejs iz JPA Criteria API Metod kojim se ostvaruje
SELECT CriteriaQuery select()
  Subquery select()
FROM AbstractQuery from()
WHERE AbstractQuery where()
ORDER BY CriteriaQuery orderBy()
GROUP BY AbstractQuery groupBy()
HAVING AbstractQuery having()

A newly created CriteriaQuery object is basically an empty shell. With the exception of defining the result type of the query, no additional content has yet been added to fill out the query. As with HQL queries, the developer is responsible for defining the various clauses of the query necessary to fetch the desired data from the database. Semantically speaking, there is no difference between HQL and Criteria API query definitions. Both have SELECT, FROM, WHERE, GROUP BY, HAVING, and ORDER BY clauses; only the manner of defining them is different. Before we can fill in the various clauses of the query definition, let’s first revisit two key concepts and look at the equivalent Criteria API syntax for those concepts.

12.3.1 Koreni upita

The first fundamental concept to revisit is the identification variable used in the FROM clause of HQL queries to alias declarations that cover entity, embeddable, and other abstract schema types. In HQL, the identification variable takes on a central importance, as it is the key to tying the different clauses of the query together. But with the Criteria API we represent query components with objects and therefore rarely have aliases with which to concern ourselves. Still, in order to define a FROM clause, we need a way to express which abstract schema types we are interested in querying against.

Interfejs AbstractQuery (roditelj interfejsa CriteriaQuery) nudi metod from() za definisanje apstraktne sheme koja formira osnovu našeg upita. Ovaj metod prihvata tip entiteta kao parametar i dodaje novi koren u upitu. Koren u upitu koresponsira identifikacionoj promenljivoj u HQL-u, koja dalje koresponsira deklaraciji ranga (tj. skupu iz kojeg se podaci izdvajaju) ili izrazu spajanja. Naredni kod ilustruje kako možemo dobiti koren upita:

CriteriaQuery<Student> c    = cb.createQuery(Student.class);
Root<Student> emp           = c.from(Student.class);

Metod from() vraća objekat interfejsa Root koja odgovara tipu entiteta. Interfejs Root je dete interfejsa From, koji nam nudi funkcionalnosti spajanja. Interfejs From je dete interfejsa Path, koji je dalje dete Expression, pa Selection, kojim se omogućava da se koren koristi u drugim delovima definicije upita. Uloga svih ovih interfejsa će biti detaljnije opisana u kasnijim sekcijama.

Pozivi metoda from() su aditivni. To znači da svaki poziv dodaje novi koren u upitu, što rezultuje u Dekartovom proizvodu kada je više od jednog korena definisan, ako nema daljih ograničenja u WHERE klauzi. Naredni primer demonstrira višekoreni upit, koji zamenjuju savremenija spajanja tradacionalnijim SQL pristupom:

SELECT  DISTINCT s
FROM    Student s, 
        Ispit i
WHERE   s = i.indeks

Da bismo pretvorili ovaj upit u JPA Criteria API, potrebno je da pozovemo metod from() dvaput, dodajući entitete Student i Ispit kao korene upita:

CriteriaQuery<Student> c    = cb.createQuery(Student.class);
Root<Student> stud          = c.from(Student.class);
Root<Ispit> ispit           = c.from(Ispit.class);

c.select(stud)
 .distinct(true)
 .where(cb.equal(stud, ispit.get("indeks")));

12.3.2 Izrazi nad putanjama

The second fundamental concept to revisit is the path expression. The path expression is the key to the power and flexibility of the HQL language, and it is likewise a central piece of the Criteria API.

Govorili smo o korenima upita u prethodnoj podsekciji, ali svi koreni zapravo predstavljaju specijalni tip izraza putanje. Posmatrajmo naredni jednostavan HQL upit koji vraća sve studente koji su upisani na studijskom programu Informatika:

SELECT  s
FROM    Student s
WHERE   s.studijskiProgram.naziv = 'Informatika'

Razmišljajući u terminima JPA Criteria API, koren ovog izraza je entitet Student i neka je on predstavljen objektom stud. Ovaj upit takođe sadrži izraz putanje u WHERE klauzi. Da bismo predstavili ovaj izraz putanje korišćenjem JPA Criteria API, koristimo naredni izraz:

stud.get("studijskiProgram").get("naziv")

Metod get() je izveden iz interfejsa Path proširenog interfejsom Root i ekvivalentan je operatoru tačke koji se koristi u HQL izrazima putanje radi navigacije kroz putanju. S obzirom da metod get() vraća objekat interfejsa Path, pozivi metoda se mogu ulančavati, što će nam biti veoma korisna tehnika pri izgradnji upita, jer time izbegavamo uvođenje privremenih lokalnih promenljivih (koji nam ne služe praktično ničemu). Argument metoda get() je naziv atributa entiteta za koji smo zainteresovani. Zbog toga što je rezultat konstruisanja izraza putanje objekat interfejsa Expression koji mi možemo da koristimo za izgradnju uslovnih izraza, možemo prethodni HQL upit izraziti na sledeći način:

CriteriaQuery<Student> c    = cb.createQuery(Student.class);
Root<Student> stud          = c.from(Student.class);

c.select(stud)
 .where(cb.equal(stud.get("studijskiProgram").get("naziv"), "Informatika"));

Much like HQL, path expressions may be used throughout the different clauses of the query definition. With the Criteria API, it is necessary to hold onto the root object in a local variable and use it to form path expressions where required. Once again it is worth emphasizing that the from() method of AbstractQuery should never be invoked more than once for each desired root. Invoking it multiple times will result in additional roots being created and a Cartesian product if not careful. Always store the root objects locally and refer to them when necessary.

12.4 Klauza SELECT

Postoji nekoliko formi koje klauza SELECT može da uzima u upitu. Najjednostavnija forma predstavlja izdvajanje jednog izraza, dok neke druge forme rade sa više izraza ili koriste konstruktorskih izraza radi kreiranja novih instancnih objekata. Svaka forma je na drugi način predstavljena u JPA Criteria API.

12.4.1 Izdvajanje jednog izraza

Metod select() interfejsa CriteriaQuery se koristi radi formiranja SELECT klauze u JPA Criteria API definicijama upita. Sve forme SELECT klauze se mogu predstaviti metodom select(), premda postoje bolji načini korišćenjem specijalizovanih metoda radi pojednostavljivanja kodiranja. Metod select() prihvata argument interfejsa Selection, što je zapravo roditelj interfejsa Expression, kao i CompoundSelection za slučaj kada je rezultat upita Tuple ili niz rezultata.

Do sada, mi smo prosleđivali koren upita metodu select(), čime smo indikovali da želimo da odgovarajući entitet bude rezultat upita. Moguće je proslediti i izraz koji predstavlja jednu vrednost, poput izdvajanja atributa iz entiteta ili bilo kog kompatibilnog skalarnog izraza. Naredni primer ilustruje ovaj pristup izdvajanjem atributa koji predstavlja ime entiteta Student:

CriteriaQuery<String> c = cb.createQuery(String.class);
Root<Student> stud      = c.from(Stud.class);

c.select(stud.<String>get("ime"));

Ovakav upit će vratiti sva imena studenata, uključujući sve duplikate. O izdvajanju upita bez duplikata će biti reči kasnije.

Primetimo neobičnu sintaksu koju koristimo da bismo deklarisali da atribut ime predstavlja instancu tipa String. Tip izraza koji se prosleđuje metodu select() mora biti kompatibilan sa rezultujućim tipom koji se koristi za kreiranje CriteriaQuery objekta. Na primer, ako je CriteriaQuery objekat kreiran pozivom createQuery(Ispit.class) nad objektom interfejsa CriteriaBuilder, onda će biti greška ukoliko bismo pokušali da postavimo da se dobija izraz entiteta Student korišćenjem metoda select(). Kada poziv metoda poput select() koristi šablonsku tipiziranost da bi omogućio ograničenja kompatibilnosti, onda možemo prefiksovati tip imenu metoda kako bismo ga kvalifikovali u slučajevima kada tip ne može biti automatski zaključen. U ovom slučaju, moramo koristiti ovaj pristup s obzirom da je metod select() deklarisan kao:

CriteriaQuery<T> select(Selection<? extends T> selection);

Argument metoda select() mora biti tipa koji je kompatibilan rezultujućim tipom definicije upita. Metod get() vraća objekat Path, ali taj objekat je uvek tipa Path<Object> zato što kompilator ne može da zaključi odgovarajući tip samo na osnovu imena atributa. Zbog toga, da bismo deklarisali da je atribut ime u entitetu Student tipa String, moramo da preciziramo poziv metoda kao što smo videli.

This syntax has to be used whenever the Path is being passed as an argument for which the parameter has been strongly typed, such as the argument to the select() method and certain CriteriaBuilder expression methods. We have not had to use them so far in our examples because we have been using them in methods like equal(), where the parameter was declared to be of type Expression<?>. Because the type is wildcarded, it is valid to pass in an argument of type Path<Object>. Later in the chapter, we look at the strongly typed versions of the Criteria API methods that remove this requirement.

12.4.2 Izdvajanje više izraza

Kada definišemo SELECT klauzu koja sadrži izdvajanje više od jednog izraza, onda ćemo koristiti različite pristupe u zavisnosti od načina na koji je definicija upita kreirana:

Ovi objekti se kreiraju pozivima metoda tuple(), construct() i array() nad objektom interfejsa CriteriaBuilder, redom. Neka je potrebno izdvojiti godine i oznake svih ispitnih rokova, što se izvodi narednim HQL upitom:

SELECT  ir.godina,
        ir.oznaka
FROM    IspitniRok ir

Naredni primer demonstrira kako možemo imati višestruke izraze korišćenjem upita nad Tuple:

CriteriaQuery<Tuple> c  = cb.createTupleQuery();
Root<IspitniRok> ir     = c.from(IspitniRok.class);

c.select(cb.tuple(ir.get("godina"), 
                  ir.get("oznaka")));

Kao pogodnost, metod multiselect() iz interfejsa CriteriaQuery može se takođe koristiti za ispunjavanje istog zahteva. Metod multiselect() će kreirati odgovarajući tip argumenata za dati rezultujući tip upita, odnosno, zavisno od načina kreiranja upita.

Prva forma je za upite koje imaju Object ili Object[] kao njihov rezultujući tip. Metodu multiselect() se u ovom slučaju može proslediti lista izraza koje predstavljaju svaki od izraza:

CriteriaQuery<Object[]> c   = cb.createQuery(Object[].class);
Root<IspitniRok> ir         = c.from(IspitniRok.class);

c.multiselect(ir.get("godina"), 
              ir.get("oznaka"));

Note that, if the query result type is declared as Object instead of Object[], the behavior of multiselect() in this form changes slightly. The result is always an instance of Object, but if multiple arguments are passed into multiselect() then the result must be cast to Object[] in order to access any particular value. If only a single argument is passed into multiselect(), then no array is created and the result may be cast directly from Object to the desired type. In general, it is more convenient to be explicit about the query result type. If you want to work with an array of results, then declaring the query result type to be Object[] avoids casting later and makes the shape of the result more explicit if the query is invoked separately from the code that creates it.

Druga forma je slična prvoj formi, samo se odnosi na upite koji rezultuju tipom Tuple. Ponovo, metodu multiselect() se u ovom slučaju može proslediti lista izraza koje predstavljaju svaki od izraza:

CriteriaQuery<Tuple> c  = cb.createTupleQuery();
Root<IspitniRok> ir   = c.from(IspitniRok.class);

c.multiselect(ir.get("godina"), 
              ir.get("oznaka"));

Treća i konačna forma je za upite sa konstruktorskih izrazima čiji rezultat je neperzistentnog tipa. Metod multiselect() se ponovo poziva sa listom izraza, ali koristi tip upita da odredi i automatski kreira odgovarajući konstruktorski izraz, u ovom slučaju primarni ključ klase IspitniRokId:

CriteriaQuery<IspitniRokId> c   = cb.createQuery(IspitniRokId.class);
Root<IspitniRok> ir             = c.from(IspitniRok.class);

c.multiselect(ir.get("godina"), 
              ir.get("oznaka"));

Ovo je ekvivalentno narednom kodu:

CriteriaQuery<IspitniRokId> c   = cb.createQuery(IspitniRokId.class);
Root<IspitniRok> ir             = c.from(IspitniRok.class);

c.select(cb.construct(IspitniRokId.class, // Izdvoji primarni kljuc na osnovu kolona:
         ir.get("godina"),                // 1. godina
         ir.get("oznaka")));              // 2. godina

Međutim, iako je metod multiselect() izuzetno pogodan za upotrebu konstruktorskih izraza, postoje slučajevi kada je neophodno korišćenje metoda construct() iz interfejsa CriteriaBuilder. Na primer, ukoliko je potrebno da upit izračuna i naziv ispitnih rokova pored godine i oznake rokova. U tom slučaju, ispitni rokovi koji se konstruišu predstavljaju samo deo rezultata, tako da rezultujući tip upita mora biti tipa Object[] i onda on mora da sadrži konstruktorski izraz za konstruisanje instanci klase IspitniRokId:

CriteriaQuery<Object[]> c   = cb.createQuery(Object[].class);
Root<IspitniRok> ir         = c.from(IspitniRok.class);

c.multiselect(ir.get("naziv"), // Izdvoji naziv roka
              cb.construct(IspitniRokId.class, // Izdvoji primarni kljuc na osnovu kolona:
                           ir.get("godina"),   // 1. godina
                           ir.get("oznaka"))); // 2. oznaka

12.4.3 Korišćenje alijasa

Like HQL, aliases may also be set on expressions in the SELECT clause, which will then be included in the resulting SQL statement. They are of little use from a programming perspective as we construct the ORDER BY clause through the use of the Selection objects used to construct the SELECT clause.

Aliases are useful when the query has a result type of Tuple. The aliases will be available through the resulting Tuple objects. To set an alias, the alias() method of the Selection interface (parent to Expression) must be invoked. The following example demonstrates this approach:

CriteriaQuery<Tuple> c  = cb.createTupleQuery();
Root<Student> stud      = c.from(Student.class);
c.multiselect(stud.get("indeks").alias("indeks"), 
              stud.get("mesto_rodjenja").alias("mesto"));

This example actually demonstrates two facets of the alias() method. The first is that it returns itself, so it can be invoked as part of the call to select() or multiselect(). The second is, once again, that it returns itself, and is therefore mutating what should be an otherwise immutable object. The alias() method is an exception to the rule that only the query definition interfaces—CriteriaQuery and Subquery—contain mutating operations. Invoking alias() changes the original Selection object and returns it from the method invocation. It is invalid to set the alias of a Selection object more than once.

Using the alias when iterating over the query results is as simple as requesting the expression by name. Executing the previous query would allow it to be processed as follows:

TypedQuery<Tuple> q = session.createQuery(c);
for (Tuple t : q.getResultList()) {
    Integer indeks  = t.get("indeks", Integer.class);
    String mesto    = t.get("mesto", String.class);
    // ...
}

12.5 Klauza FROM

U sekciji 12.3.1, diskutovali smo o metodu from() iz interfejsa AbstractQuery kao i o ulogama korenova upita u formiranju definicije upita. Sada ćemo proširiti tu diskusiju i prikazati načine na koje se izrazi spajanja izražavaju u JPA Criteria API.

12.5.1 Unutrašnja i spoljašnja spajanja

Izrazi spajanja se kreiraju korišćenjem metoda join() iz interfejsa From koji predstavlja roditelj interfejsa Root, o kojem smo diskutovali ranije, kao i interfejsa Join, koji predstavlja tip objekata koji se vraćaju kreiranjem izraza spajanja. Ovo znači da proizvoljan koren upita može da se spaja, kao i da spajanja mogu da se ulančavaju. Metod join() zahteva izraz putanje kao argument i opciono, argument kojim se specifikuje tip spajanja: JoinType.INNER ili JoinType.LEFT, za unutrašnja ili spoljašnja spajanja, redom.

Kada se vrše spajanja unakrst kolekcijskih tipova (sa izuzetkom Map), spajanje će imati dva parametarska tipa: tip izvora i tip cilja. Ovim se održava sigurnost tipova sa obe strane spajanja i

Metod join() je aditivan, što znači da svaki poziv rezultuje novim spajanjem koji se kreira. Zbog toga, instanca tipa Join koja se vraća kao povratna vrednost metoda bi trebalo biti sačuvana kao lokalna promenljiva radi formiranja izraza putanja kasnije.

Naredni primer ilustruje spoljašnje spajanje od Student ka Ispit:

Root<Student> stud          = c.from(Student.class);
Join<Student,Ispit> ispit   = stud.join("indeks", JoinType.LEFT);

Da smo izostavili argument JoinType.LEFT, spajanje bi podrazumevano bilo unutrašnje. Poput HQL-a, višestruka spajanja se mogu asocirati za istu From instancu. Na primer, za navigaciju kroz Ispit i StudijskiProgram, potrebno je koristiti naredni kod, koji pretpostavlja unutrašnja spajanja:

Root<Student> stud          = c.from(Student.class);
Join<Student,Ispit> ispit   = stud.join("indeks");
Join<Student,StudijskiProgram> studijskiProgram     = stud.join("studijskiProgram");

Spajanja se takođe mogu ulančavati u jednu naredbu. Rezultujuće spajanje će biti tipizirano tipom izvora i tipom cilja poslednjeg spajanja u naredbi:

Root<StudijskiProgram> studijskiProgram             = c.from(StudijskiProgram.class);
Join<Student,Ispit> ispiti  = studijskiProgram.join("studenti").join("ispiti");

Joins across collection relationships that use Map are a special case. HQL uses the KEY and VALUE keywords to extract the key or value of a Map element for use in other parts of the query. In the JPA Criteria API, these operators are handled by the key() and value() methods of the MapJoin interface. Consider the following example assuming a Map join across the phones relationship of the Employee entity:

SELECT  e.name, 
        KEY(p), 
        VALUE(p)
FROM    Employee e JOIN 
        e.phones p

To create this query using the JPA Criteria API, we need to capture the result of the join as a MapJoin, in this case using the joinMap() method. The MapJoin object has three type parameters: the source type, key type, and value type. It can look a little more daunting, but makes it explicit what types are involved.

CriteriaQuery<Object> c                 = cb.createQuery();
Root<Employee> emp                      = c.from(Employee.class);
MapJoin<Employee,String,Phone> phone    = emp.joinMap("phones");

c.multiselect(emp.get("name"), 
              phone.key(), 
              phone.value());

We need to use the joinMap() method in this case because there is no way to overload the join() method to return a Join object or MapJoin object when all we are passing in is the name of the attribute. Collection, Set, and List relationships are likewise handled with the joinCollection(), joinSet(), and joinList() methods for those cases where a specific join interface must be used. The strongly typed version of the join() method, which we demonstrate later, is able to handle all join types though the single join() call.

12.6 Klauza WHERE

Kao što smo videli u nekim primerima, klauza WHERE u JPA Criteria API upitu se postavlja metodom where() iz interfejsa AbstractQuery. Metod where() ima dve varijante u zavisnosti od toga šta prihvata:

Svaki poziv metoda where() će poništiti sve druge WHERE klauze koje su postavljene prethodnim pozivima metoda where() i konstruisaće nov skup ograničenja na osnovu novoprosleđenih kriterijuma.

Definicija WHERE klauze je data u nastavku:

where_clause ::= WHERE conditional_expression

Sada ćemo videti na koje načine je moguće konstruisati ograničenja koja se prosleđuju metodu where().

12.6.1 Konstrukcija izraza

Ključ za konstruisanje izraza u okviru JPA Criteria API je interfejs CriteriaBuilder. Ovaj interfejs sadrži metode za sve predikate, izraze i funkcije koje HQL podržava kao i druge karakteristike specifične za JPA Criteria API. Naredne tabele sumiraju preslikavanja između HQL operatora, izraza i funkcija u njihove ekvivalentne metode interfejsa CriteriaBuilder. Primetimo da u nekim slučajevima ne postoji direktna jednakost između HQL operatora i metoda, već je neophodno koristiti kombinaciju metoda iz CriteriaBuilder kako bismo ostvarili isti rezultat. U nekim drugim slučajevima, ekvivalentan metod za JPA Criteria API se nalazi u interfejsu koji nije CriteriaBuilder.

HQL to CriteriaBuilder Predicate Mapping:

HQL Operator CriteriaBuilder Method
AND and()
OR or()
NOT not()
= equal()
<> notEqual()
> greaterThan(),gt()
>= greaterThanOrEqualTo(),ge()
< lessThan(),lt()
<= lessThanOrEqualTo(),le()
BETWEEN between()
IS NULL isNull()
IS NOT NULL isNotNull()
EXISTS exists()
NOT EXISTS not(exists())
IS EMPTY isEmpty()
IS NOT EMPTY isNotEmpty()
MEMBER OF isMember()
NOT MEMBER OF isNotMember()
LIKE like()
NOT LIKE notLike()
IN in()
NOT IN not(in())

HQL to CriteriaBuilder Scalar Expression Mapping:

HQL Expression CriteriaBuilder Method
ALL all()
ANY any()
SOME some()
- neg(),diff()
+ sum()
* prod()
/ quot()
COALESCE coalesce()
NULLIF nullif()
CASE selectCase()

HQL to CriteriaBuilder Function Mapping:

HQL Function CriteriaBuilder Method
ABS abs()
CONCAT concat()
CURRENT_DATE currentDate()
CURRENT_TIME currentTime()
CURRENT_TIMESTAMP currentTimestamp()
LENGTH length()
LOCATE locate()
LOWER lower()
MOD mod()
SIZE size()
SQRT sqrt()
SUBSTRING substring()
UPPER upper()
TRIM trim()

HQL to CriteriaBuilder Aggregate Function Mapping:

HQL Aggregate Function CriteriaBuilder Method
AVG avg()
SUM sum(),sumAsLong(),sumAsDouble()
MIN min(),least()
MAX max(),greatest()
COUNT count()
COUNT DISTINCT countDistinct()

Pored direktnog prevođenja HQL operatora, izraza i funkcija, postoje neke tehnike koje su specifične za JPA Criteria API koje je potrebno uzeti u obzir prilikom konstrukcije izraza. Sledeće sekcije detaljno razmatraju ove tehnike.

12.6.2 Predikati

U narednom primeru, prosledili smo niz objekata Predicate metodu and(). Ovim se dobija ponašanje koje kombinuje sve izraze operatorom AND. Ekvivalentno ponašanje, samo za operatom OR, ostvaruje se metodom or(). Postoji prečica koja radi za AND operator, a to je da se svi izrazi proslede kao argumenti metodu where(). Ovaj pristup će implicitno kombinovati izraze korišćenjem semantike operatora AND.

List<Predicate> criteria = new ArrayList<Predicate>();
if (name != null) {
    ParameterExpression<String> p = cb.parameter(String.class, "name");
    criteria.add(cb.equal(emp.get("name"), p));
}
if (deptName != null) {
    ParameterExpression<String> p = cb.parameter(String.class, "dept");
    criteria.add(cb.equal(emp.get("dept").get("name"), p));
}

if (criteria.size() == 0) {
    throw new RuntimeException("no criteria");
} else if (criteria.size() == 1) {
    c.where(criteria.get(0));
} else {
    c.where(cb.and(criteria.toArray(new Predicate[0])));
}

TypedQuery<Employee> q = em.createQuery(c);
if (name != null) { q.setParameter("name", name); }
if (deptName != null) { q.setParameter("dept", deptName); }

// ...

The Criteria API also offers a different style of building AND and OR expressions for those who wish to build things incrementally rather than as a list. The conjunction() and disjunction() methods of the CriteriaBuilder interface create Predicate objects that always resolve to true and false, respectively. Once obtained, these primitive predicates can then be combined with other predicates to build up nested conditional expressions in a tree-like fashion. The following example rewrites the predication construction portion of the previous example using the conjunction() method. Note how each conditional statement is combined with its predecessor using an and() call.

Predicate criteria = cb.conjunction();
if (name != null) {
    ParameterExpression<String> p =
        cb.parameter(String.class, "name");
    criteria = cb.and(criteria, cb.equal(emp.get("name"), p));
}
if (deptName != null) {
    ParameterExpression<String> p =
        cb.parameter(String.class, "dept");
    criteria = cb.and(criteria,
                      cb.equal(emp.get("dept").get("name"), p));
}

// ...

Što se tiče ostalih predikata, u tabelama za predikate iznad treba napomenuti da postoje dva skupa metoda koji su dostupni za relaciona poređenja. Na primer, postoje metodi greaterThan() i gt(). Metodi koji imaju naziv od dva karaktera su specifični za numeričke vrednosti i strogo su tipizirani da rade sa brojevima. Metodi koji pripadaju skupu sa dužim nazivom se mogu koristiti u svim ostalim slučajevima.

12.6.3 Doslovne vrednosti

Literal values may require special handling when expressed with the JPA Criteria API. In all the cases encountered so far, methods are overloaded to work with both Expression objects and Java literals. However, there may be some cases where only an Expression object is accepted (in cases where it is assumed you would never pass in a literal value or when any of a number of types would be acceptable). If you encounter this situation then, to use these expressions with Java literals, the literals must be wrapped using the literal() method.

NULL doslovne vrednosti se kreiraju pozivom metoda nullLiteral(), koji prihvata tip klase kao parametar i proizvodi tipiziranu verziju vrednosti NULL za datu prosleđenu klasu. Ovo je neophodno radi proširivanja stroge tipiziranosti API-ja na NULL vrednosti.

12.6.4 Parametri (parametarske oznake)

Upravljanje parametrima u JPA Criteria API upitima se razlikuje od pristupa u HQL-u. Ako se prisetimo, u HQL-u upitima se parametri navode dvotačkom koju prati naziv parametra. U JPA Criteria API-u ovo ne funkcioniše. Umesto toga, moramo da eksplicitno kreiramo ParameterExpression korektnog tipa koji se može koristiti u uslovnim izrazima. Ovo se izvodi metodom parameter() interfejsa CriteriaBuilder. Ovaj metod zahteva tip klase (kako bi postavio tip objekta ParameterExpression) i opciono ime koje će koristiti u slučaju imenovanih parametara. Neka je potrebno izdvojiti podatke narednim upitom:

SELECT  s
FROM    Student s
WHERE   s.studijskiProgram.naziv = :ns

Naredni kod ilustruje korišćenje ovog metoda:

CriteriaQuery<Student> c    = cb.createQuery(Student.class);
Root<Student> stud          = c.from(Student.class);

c.select(stud);

ParameterExpression<String> nazivStudijskogPrograma =
    cb.parameter(String.class, "ns");

c.where(cb.equal(stud.get("studijskiProgram").get("naziv"), 
                 nazivStudijskogPrograma));

Ako se parametar neće koristiti u drugim delovima upita, onda ga je moguće ugnezditi direktno u predikatskom izrazu da bismo učinili ceo upit konciznijim. Naredni kod ilustruje ovu tehniku:

CriteriaQuery<Student> c    = cb.createQuery(Student.class);
Root<Student> stud           = c.from(Employee.class);

c.select(stud)
 .where(cb.equal(stud.get("studijskiProgram").get("naziv"),
                 cb.parameter(String.class, "ns")));

Da bismo izvršili ovaj upit, očigledno, neophodno je da postavimo vrednost datog parametra. U prvom pristupu, možemo koristiti preopterećenje koje prihvata ParameterExpression objekat:

TypedQuery<Student> query = session.createQuery(c);
query.setParameter(nazivStudijskogPrograma, "Informatika");
List<Student> results = query.getResultList();

U drugom pristupu, možemo koristiti preopterećenje koje prihvata String koji sadrži naziv imenovanog parametra:

TypedQuery<Student> query = session.createQuery(c);
query.setParameter("ns", "Informatika");
List<Student> results = query.getResultList();

12.6.5 Podupiti

The AbstractQuery interface provides the subquery() method for creation of subqueries. Subqueries may be correlated (meaning that they reference a root, path, or join from the parent query) or non-correlated. The JPA Criteria API supports both correlated and non-correlated subqueries, again using query roots to tie the various clauses and expressions together. The argument to subquery() is a class instance representing the result type of the subquery. The return value is an instance of Subquery, which is itself an extension of AbstractQuery. With the exception of restricted methods for constructing clauses, the Subquery instance is a complete query definition like CriteriaQuery that may be used to create both simple and complex queries.

To demonstrate subquery usage, let’s look at a more significant example which uses subqueries to eliminate duplicates. The Employee entity has relationships with four other entities: single-valued relationships with Department and Address, and collection-valued relationships with Phone and Project. Whenever we join across a collection-valued relationship, we have the potential to return duplicate rows; therefore, we need to change the criteria expression for Project to use a subquery. The following code fragment shows the required code:

CriteriaBuilder cb          = em.getCriteriaBuilder();
CriteriaQuery<Employee> c   = cb.createQuery(Employee.class);
Root<Employee> emp = c.from(Employee.class);

c.select(emp);

// ...

if (projectName != null) {
    Subquery<Employee> sq           = c.subquery(Employee.class);
    Root<Project> project           = sq.from(Project.class);
    Join<Project,Employee> sqEmp    = project.join("employees");

    sq.select(sqEmp)
      .where(cb.equal(project.get("name"),
                      cb.parameter(String.class, "project")));
    criteria.add(cb.in(emp).value(sq));
}

// ...

We introduced a new non-correlated subquery against Project. Because the subquery declares its own root and does not reference anything from the parent query, it runs independently and is therefore non-correlated. The equivalent HQL query with only Project criteria would be:

SELECT  e
FROM    Employee e
WHERE   e IN (
            SELECT  emp
            FROM    Project p JOIN 
                    p.employees emp
            WHERE   p.name = :project
        )

Whenever we write queries that use subqueries, there is often more than one way to achieve a particular result. For example, we could rewrite the previous example to use EXISTS instead of IN and shift the conditional expression into the WHERE clause of the subquery:

if (projectName != null) {
    Subquery<Project> sq            = c.subquery(Project.class);
    Root<Project> project           = sq.from(Project.class);
    Join<Project,Employee> sqEmp    = project.join("employees");

    sq.select(project)
      .where(cb.equal(sqEmp, emp),
             cb.equal(project.get("name"),
                      cb.parameter(String.class,"project")));
    criteria.add(cb.exists(sq));
}

By referencing the Employee root from the parent query in the WHERE clause of the subquery, we now have a correlated subquery. This time the query takes the following form in HQL:

SELECT  e
FROM    Employee e
WHERE   EXISTS (
            SELECT  p
            FROM    Project p JOIN 
                    p.employees emp
            WHERE   emp = e AND 
                    p.name = :name
        )

We can still take this example further and reduce the search space for the subquery by moving the reference to the Employee root to the FROM clause of the subquery and joining directly to the list of projects specific to that employee. In HQL, we would write this as follows:

SELECT  e
FROM    Employee e
WHERE   EXISTS (
            SELECT  p
            FROM    e.projects p
            WHERE   p.name = :name
        )

In order to re-create this query using the JPA Criteria API, we are confronted with a dilemma. We need to base the query on the Root object from the parent query but the from() method only accepts a persistent class type. The solution is the correlate() method from the Subquery interface. It performs a similar function to the from() method of the AbstractQuery interface, but does so with Root and Join objects from the parent query. The following example demonstrates how to use correlate() in this case:

if (projectName != null) {
    Subquery<Project> sq            = c.subquery(Project.class);
    Root<Employee> sqEmp            = sq.correlate(emp);
    Join<Employee,Project> project  = sqEmp.join("projects");

    sq.select(project)
      .where(cb.equal(project.get("name"),
             cb.parameter(String.class,"project")));
    criteria.add(cb.exists(sq));
}

Before we leave subqueries in the JPA Criteria API, there is one more corner case with correlated subqueries to explore: referencing a join expression from the parent query in the FROM clause of a subquery. Consider the following example that returns projects containing managers with direct reports earning an average salary higher than a userdefined threshold:

SELECT  p
FROM    Project p JOIN 
        p.employees e
WHERE   TYPE(p) = DesignProject AND
        e.directs IS NOT EMPTY AND
        (SELECT AVG(d.salary)
         FROM   e.directs d) >= :value

When creating the Criteria API query definition for this query, we must correlate the employees attribute of Project and then join it to the direct reports in order to calculate the average salary. This example also demonstrates the use of the type() method of the Path interface in order to do a polymorphic comparison of types:

CriteriaQuery<Project> c        = cb.createQuery(Project.class);
Root<Project> project           = c.from(Project.class);
Join<Project,Employee> emp      = project.join("employees");
Subquery<Number> sq             = c.subquery(Number.class);
Join<Project,Employee> sqEmp    = sq.correlate(emp);
Join<Employee,Employee> directs = sqEmp.join("directs");

c.select(project)
  .where(cb.equal(project.type(), DesignProject.class),
         cb.isNotEmpty(emp.<Collection>get("directs")),
         cb.ge(sq.select(cb.avg(directs.get("salary"))),
               cb.parameter(Number.class, "value")));

12.6.6 Izraz IN

Za razliku od drugih operatora, operator IN zahteva specijalno upravljanje u JPA Criteria API. Metod in() interfejsa CriteriaBuilder prihvata samo jedan argument-jednovrednosni izraz koji će biti testiran nad skupom vrednosti u IN izrazu. Da bismo postavili skup vrednosti, potrebno je da koristimo CriteriaBuilder.In objekat koji se dobija kao povratna vrednost metoda in(). Posmatrajmo naredni HQL upit:

SELECT  s
FROM    Student s
WHERE   s.studijskiProgram.naziv IN ('Informatika', 'Matematika', 'Astronomija')

Da bismo pretvorili ovaj upit u JPA Criteria API upit, potrebno je da pozovemo metod value() iz interfejsa CriteriaBuilder.In da bismo postavili skup vrednosti za nazive studijskih programa za koje smo zainteresovani, kao u narednom kodu:

CriteriaQuery<Student> c   = cb.createQuery(Student.class);
Root<Student> stud         = c.from(Student.class);

c.select(stud)
 .where(cb.in(stud.get("studijskiProgram").get("naziv"))
          .value("Informatika").value("Matematika").value("Astronomija"));

Primetimo ulančane pozive metoda value() kako bismo postavili skup vrednosti za IN izraz. Argument metoda in() je izraz koji će biti upoređen sa vrednostima iz skupa vrednosti koje su postavljene metodom value().

Specijalno, u slučajevima kada postoji veliki broj poziva value() metoda od kojih su sve vrednosti istog tipa, interfejsa Expression omogućava prečicu za kreiranje IN izraza. Metod in() iz ovog interfejsa dozvoljava jednu ili više vrednosti da budu postavljene jednim pozivom, što naredni kod ilustruje:

CriteriaQuery<Student> c   = cb.createQuery(Student.class);
Root<Student> stud         = c.from(Student.class);

c.select(stud)
 .where(stud.get("studijskiProgram").get("naziv")
            .in("Informatika", "Matematika", "Astronomija"));

U ovom pristupu, poziv metoda in() predstavlja ulančani sufiks nad izrazom umesto prefiksni, kao što je to bilo u prethodnom primeru. Primetimo razliku u tipovima argumenata između dve verzije metoda in() iz interfejsa CriteriaBuilder i Expression. Verzija iz interfejsa Expression prihvata skup vrednosti, a ne izraz koji će biti upoređen. Sa druge strane, verzija iz interfejsa CriteriaBuilder dozvoljava više tipizirani pristup. Odabir između ova dva pristupa je uglavnom stvar preference ili konvencije koju kompanija koristi.

IN expressions that use subqueries are written using a similar approach. For a more complex example, in the previous chapter, we demonstrated a HQL query using an IN expression in which the department of an employee is tested against a list generated from a subquery. The example is reproduced here.

SELECT  e
FROM    Employee e
WHERE   e.department IN (
            SELECT  DISTINCT d
            FROM    Department d JOIN 
                    d.employees de JOIN 
                    de.project p
            WHERE   p.name LIKE 'QA%'
        )

We can convert this example to the JPA Criteria API, as shown in the following code:

CriteriaQuery<Employee> c       = cb.createQuery(Employee.class);
Root<Employee> emp              = c.from(Employee.class);
Subquery<Department> sq         = c.subquery(Department.class);
Root<Department> dept           = sq.from(Department.class);
Join<Employee,Project> project  = dept.join("employees").join("projects");

sq.select(dept.<Integer>get("id"))
  .distinct(true)
  .where(cb.like(project.<String>get("name"), 
                 "QA%"));

c.select(emp)
 .where(cb.in(emp.get("dept").get("id"))
          .value(sq));

The subquery is created separately and then passed into the value() method as the expression to search for the Department entity. This example also demonstrates using an attribute expression as a value in the search list.

12.6.7 Izraz CASE

Like the IN expression, building CASE expressions with the Criteria API requires the use of a helper interface. We begin with the general form of the CASE expression, the most powerful but also the most complex.

SELECT  p.name,
        CASE 
            WHEN TYPE(p) = DesignProject THEN 'Development'
            WHEN TYPE(p) = QualityProject THEN 'QA'
            ELSE 'Non-Development'
        END
FROM    Project p
WHERE   p.employees IS NOT EMPTY

The selectCase() method of the CriteriaBuilder interface is used to create the CASE expression. For the general form, it takes no arguments and returns a CriteriaBuilder.Case object that we may use to add the conditional expressions to the CASE statement. The following example demonstrates this approach:

CriteriaQuery<Object[]> c   = cb.createQuery(Object[].class);
Root<Project> project       = c.from(Project.class);

c.multiselect(project.get("name"),
              cb.selectCase()
                .when(cb.equal(project.type(), DesignProject.class),
                      "Development")
                .when(cb.equal(project.type(), QualityProject.class),
                      "QA")
                .otherwise("Non-Development"))
 .where(cb.isNotEmpty(project.<List<Employee>>get("employees")));

The when() and otherwise() methods correspond to the WHEN and ELSE keywords from HQL. Unfortunately, else is already a keyword in Java, so otherwise must be used as a substitute.

The next example simplifies the previous example down to the simple form of the CASE statement.

SELECT  p.name,
        CASE TYPE(p)
            WHEN DesignProject THEN 'Development'
            WHEN QualityProject THEN 'QA'
            ELSE 'Non-Development'
        END
FROM    Project p
WHERE   p.employees IS NOT EMPTY

In this case, we pass the primary expression to be tested to the selectCase() method and use the when() and otherwise() methods of the CriteriaBuilder.SimpleCase interface. Rather than a predicate or boolean expression, these methods now accept single-valued expressions that are compared to the base expression of the CASE statement:

CriteriaQuery<Object[]> c   = cb.createQuery(Object[].class);
Root<Project> project       = c.from(Project.class);

c.multiselect(project.get("name"),
              cb.selectCase(project.type())
                .when(DesignProject.class, "Development")
                .when(QualityProject.class, "QA")
                .otherwise("Non-Development"))
 .where(cb.isNotEmpty(project.<List<Employee>>("employees")));

12.6.8 Izraz COALESCE

The next example we cover in this subsection concerns the HQL COALESCE expression:

SELECT  COALESCE(d.name, d.id)
FROM    Department d

Building a COALESCE expression with the JPA Criteria API requires a helper interface like the other examples we have looked at in this section, but it is closer in form to the IN expression than to the CASE expressions. Here we invoke the coalesce() method without arguments to get back a CriteriaBuilder.Coalesce object that we then use the value() method of to add values to the COALESCE expression. The following example demonstrates this approach:

CriteriaQuery<Object> c     = cb.createQuery();
Root<Department> dept       = c.from(Department.class);

c.select(cb.coalesce()
           .value(dept.get("name"))
           .value(dept.get("id")));

Convenience versions of the coalesce() method also exist for the case where only two expressions are being compared.

CriteriaQuery<Object> c     = cb.createQuery();
Root<Department> dept       = c.from(Department.class);

c.select(cb.coalesce(dept.get("name"),
                     dept.get("id")));

A final note about case expressions is that they are another exception to the rule that the CriteriaBuilder methods are non-mutating. Each when() method causes another conditional expression to be added incrementally to the case expression, and each value() method adds an additional value to the coalesce list.

12.6.9 Izrazi sa funkcijama

Not to be confused with the built-in functions of HQL, criteria function expressions are the JPA Criteria API equivalent of the FUNCTION keyword in HQL. They allow native SQL stored functions to be mixed with other JPA Criteria API expressions. They are intended for cases where a limited amount of native SQL is required to satisfy some requirement but you don’t want to convert the entire query to SQL.

Function expressions are created with the function() method of the CriteriaBuilder interface. It requires as arguments the database function name, the expected return type, and a variable list of arguments, if any, that should be passed to the function. The return type is an Expression, so it can be used in many other places within the query. The following example invokes a database function to capitalize the first letter of each word in a department name:

CriteriaQuery<String> c     = cb.createQuery(String.class);
Root<Department> dept       = c.from(Department.class);

c.select(cb.function("initcap", 
                     String.class, 
                     dept.get("name")));

As always, developers interested in maximizing the portability of their applications should be careful in using function expressions. Unlike native SQL queries, which are clearly marked, function expressions are a small part of what otherwise looks like a normal portable JPA query that is actually tied to database-specific behavior.

12.6.10 Klauza ORDER BY

Metod orderBy() interfejsa CriteriaQuery postavlja uređenje u definiciji upita. Ovaj metoda prihvata jedan ili više Order objekata, koji se kreiraju pozivom metoda asc() ili desc() interfejsa CriteriaBuilder, za kreiranje rastućih ili opadajućih uređenja, redom. Naredni primer ilustruje metod orderBy():

CriteriaQuery<Tuple> c  = cb.createQuery(Tuple.class);
Root<Student> stud      = c.from(Student.class);
Join<Student,StudijskiProgram> studijskiProgram = stud.join("studijskiProgram");

c.multiselect(studijskiProgram.get("naziv"), 
              stud.get("ime"),
              stud.get("prezime"));
c.orderBy(cb.desc(studijskiProgram.get("naziv")),
          cb.asc(stud.get("prezime")));

Argumenti metoda asc() i desc() moraju biti jednovrednosni izrazi, tipično formirani na osnovu atributa entiteta. Redosled u kojem se argumenti prosleđuju metodu orderBy() definišu generisani SQL. Ekvivalentan HQL upit za prethodni primer je:

SELECT      sp.naziv
            s.ime,
            s.prezime
FROM        Student s JOIN 
            s.studijskiProgram sp
ORDER BY    sp.naziv DESC, 
            s.prezime

12.6.11 Klauze GROUP BY i HAVING

Metodu groupBy() i having() interfejsa AbstractQuery predstavlja JPA Criteria API ekvivalente klauzama GROUP BY i HAVING u HQL-u, redom. Oba metoda prihvataju jedan ili više izraza koji se koriste za grupisanje i filtriranje podataka.

Pogledajmo kako je GROUP BY klauza definisana:

groupby_clause  ::= GROUP BY groupby_item {, groupby_item}*
groupby_item    ::= single_valued_path_expression | identification_variable

Pogledajmo kako je HAVING klauza definisana:

having_clause   ::= HAVING conditional_expression

Do ovog trenutka u poglavlju, trebalo bi da čitaocu bude intuitivan način korišćenja ovih metoda. Pogledajmo naredni HQL upit:

SELECT      s.indeks, 
            COUNT(i)
FROM        Student s JOIN 
            s.ispiti i
GROUP BY    s.indeks
HAVING      COUNT(i) BETWEEN 20 AND 30

Da bismo rekreirali ovaj primer u JPA Criteria API, potrebno je da koristimo i agregatne funkcije i metode za grupisanje. Naredni primer demonstrira ovu konverziju:

CriteriaQuery<Tuple> c      = cb.createTupleQuery();
Root<Student> stud          = c.from(Student.class);
Join<Student,Ispit> ispit   = stud.join("ispiti");

c.multiselect(stud.get("indeks"), cb.count(ispit))
 .groupBy(stud)
 .having(cb.between(cb.count(ispit), 
                    cb.literal(new Long(20)), 
                    cb.literal(new Long(30))));

Zadatak 12.1: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate i JPA Criteria API-ja izdvaja podatke o studentima bez duplikata čija imena ili prezimena počinju slovom P i koji su rođeni u Beogradu ili Kragujevcu. Rezultat urediti po mestu rođenja opadajuće, pa po imenu i prezimenu rastuće. Ispisati mesto stanovanja, indeks, ime i prezime za date studente.

Rešenje: Dajemo dva rešenja. Prvo od njih koristi pristup sa metodom select() u kojem se izdvajaju objekti klase Student. Drugo od njih koristi pristup sa metodom multiselect() da izdvoji samo informacije od značaja.

Datoteka: vezbe/primeri/poglavlje_12/src/zadatak_12_1/Main.java:

package zadatak_12_1;

import java.util.List;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.hibernate.Session;
import org.hibernate.Transaction;

public class Main {

    public static void main(String[] args) {
        System.out.println("Pocetak rada\n");

        System.out.println("--------------------------------");
        System.out.println("Izdvajanje podataka na 1. nacin:");
        readStudentiInfo();
        System.out.println("--------------------------------");
        System.out.println("Izdvajanje podataka na 2. nacin:");
        readStudentiInfoMultiselect();

        System.out.println("Zavrsetak rada\n");
        HibernateUtil.getSessionFactory().close();
    }

    private static void readStudentiInfo() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        Transaction TR = null;
        
        try {
            TR = session.beginTransaction();

            CriteriaBuilder cb = session.getCriteriaBuilder();

            // Izdvojiti podatke o studentima
            CriteriaQuery<Student> criteria = cb.createQuery(Student.class);
            Root<Student> st = criteria.from(Student.class);
            criteria.select(st);
            // Bez duplikata
            criteria.distinct(true);
            // Cije ime ili prezime pocinju na slovo 'P'
            Predicate p1 = cb.or(cb.like(st.get("ime"), "P%"), 
                                 cb.like(st.get("prezime"), "P%"));
            // Zive u Beogradu ili Kragujevcu
            Predicate p2 = cb.isNotNull(st.get("mesto"));
            Predicate p3 = cb.in(st.get("mesto")).value("Beograd").value("Kragujevac");
            criteria.where(cb.and(p1, p2, p3));
            // Rezultat urediti po mestu rodjenja opadajuce
            Order o1 = cb.desc(st.get("mesto"));
            // pa po imenu i prezimenu rastuce
            Order o2 = cb.asc(st.get("ime"));
            Order o3 = cb.asc(st.get("prezime"));
            criteria.orderBy(o1, o2, o3);

            // Dobijanje podataka o studentima koji zadovoljavaju kriterijume
            List<Student> studenti = session.createQuery(criteria).getResultList();
            for (Student s : studenti) {
                System.out.println(
                        s.getMesto().trim() + " " +
                        s.getIndeks() + " " + 
                        s.getIme().trim() + " " + 
                        s.getPrezime().trim()
                );
            }

            TR.commit();
        } catch (Exception e) {
            System.err.println("Postoji problem sa ispisivanjem informacija o studentima! Ponistavanje transakcije!");
        
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }
    
    private static void readStudentiInfoMultiselect() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        Transaction TR = null;
        
        try {
            TR = session.beginTransaction();

            CriteriaBuilder cb = session.getCriteriaBuilder();

            // Za svakog studenta
            CriteriaQuery<Object[]> criteria = cb.createQuery(Object[].class);
            Root<Student> st = criteria.from(Student.class);
            // Izdvojiti informacije o mestu rodjenja, imenu, prezimenu i indeksu
            criteria.multiselect(st.get("mesto"), st.get("ime"), st.get("prezime"), st.get("indeks"));
            criteria.distinct(true);
            
            // Cije ime ili prezime pocinju na slovo 'P'
            Predicate p1 = cb.or(cb.like(st.get("ime"), "P%"), 
                                 cb.like(st.get("prezime"), "P%"));
            // Zive u Beogradu ili Kragujevcu
            Predicate p2 = cb.isNotNull(st.get("mesto"));
            Predicate p3 = cb.in(st.get("mesto")).value("Beograd").value("Kragujevac");
            criteria.where(cb.and(p1, p2, p3));
            // Rezultat urediti po mestu rodjenja opadajuce
            Order o1 = cb.desc(st.get("mesto"));
            // pa po imenu i prezimenu rastuce
            Order o2 = cb.asc(st.get("ime"));
            Order o3 = cb.asc(st.get("prezime"));
            criteria.orderBy(o1, o2, o3);

            // Dobijanje podataka o studentima koji zadovoljavaju kriterijume
            List<Object[]> studenti = session.createQuery(criteria).getResultList();
            for (Object[] info : studenti) {
                System.out.println(
                        ((String)info[0]).trim() + " " +
                        ((String)info[1]).trim() + " " +
                        ((String)info[2]).trim() + " " +
                        ((Integer)info[3])
                );
            }

            TR.commit();
        } catch (Exception e) {
            System.err.println("Postoji problem sa ispisivanjem informacija o studentima! Ponistavanje transakcije!");
        
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }

}

Zadatak 12.2: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate i JPA Criteria API-ja izdvaja nazive svih položenih predmeta za studenta čiji se indeks učitava sa standardnog ulaza. Ispisati prvo ime i prezime studenta, pa zatim spisak njegovih položenih ispita.

Rešenje: Dodajemo implementaciju u vidu klase Predmet za tabelu PREDMET kako bismo mogli da pristupimo nazivu predmeta. Potrebno je dopuniti i klasu Ispit radi ostvarivanja veze između ove dve klase. Konačno, dodajemo klasu Predmet u listu anotiranih klasa.

Datoteka: vezbe/primeri/poglavlje_12/src/zadatak_12_2/Predmet.java:

package zadatak_12_2;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.*;

@Entity
@Table (name = "DA.PREDMET")
public class Predmet {
    @Id
    @Column
    private Integer id;
    
    @Column
    private String naziv;
    
    @OneToMany(mappedBy="predmet")
    private List<Ispit> ispiti = new ArrayList<Ispit>();
    
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getNaziv() {
        return naziv;
    }

    public void setNaziv(String naziv) {
        this.naziv = naziv;
    }

    public List<Ispit> getIspiti() {
        return ispiti;
    }

    public void setIspiti(List<Ispit> ispiti) {
        this.ispiti = ispiti;
    }
}

Datoteka: vezbe/primeri/poglavlje_12/src/zadatak_12_2/Ispit.java:

package zadatak_12_2;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import javax.persistence.MapsId;
import javax.persistence.Table;

@Entity
@Table(name = "DA.ISPIT")
public class Ispit {
    // Primarni kljuc
    @Id
    private IspitId idIspita;

    // Ostale kolone

    @Column
    private Integer ocena;

    @Column
    private String status;
    
    @ManyToOne
    @MapsId("indeks")
    @JoinColumn(name="INDEKS", referencedColumnName="INDEKS")
    private Student student;

    @MapsId("idRoka")
    @JoinColumns({ @JoinColumn(name = "skgodina", referencedColumnName = "skgodina"),
            @JoinColumn(name = "oznakaroka", referencedColumnName = "oznakaroka") })
    @ManyToOne
    private IspitniRok ispitniRok;
    
    @ManyToOne
    @JoinColumn(name="idPredmeta",referencedColumnName="id", insertable=false, updatable=false)
    private Predmet predmet;

    // Get/set metodi

    public IspitId getIdIspita() {
        return idIspita;
    }

    public void setIdIspita(IspitId idIspita) {
        this.idIspita = idIspita;
    }

    public Integer getOcena() {
        return ocena;
    }

    public void setOcena(Integer ocena) {
        this.ocena = ocena;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

    public Predmet getPredmet() {
        return predmet;
    }

    public void setPredmet(Predmet predmet) {
        this.predmet = predmet;
    }

}

Datoteka: vezbe/primeri/poglavlje_12/src/zadatak_12_2/HibernateUtil.java:

package zadatak_12_2;

import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

class HibernateUtil {
    private static SessionFactory sessionFactory = null;

    static {
        try {
            StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
            sessionFactory = new MetadataSources(registry)
                    .addAnnotatedClass(StudijskiProgram.class)
                    .addAnnotatedClass(IspitniRok.class)
                    .addAnnotatedClass(Student.class)
                    .addAnnotatedClass(Ispit.class)
                    .addAnnotatedClass(Predmet.class)
                    .buildMetadata().buildSessionFactory();
        } catch (Throwable e) {
            System.err.println("Session factory error");
            e.printStackTrace();

            System.exit(1);
        }
    }

    static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

}

Datoteka: vezbe/primeri/poglavlje_12/src/zadatak_12_2/Main.java:

package zadatak_12_2;

import java.util.List;
import java.util.Scanner;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;

import org.hibernate.Session;
import org.hibernate.Transaction;

public class Main {

    public static void main(String[] args) {
        System.out.println("Pocetak rada\n");

        try (Scanner ulaz = new Scanner(System.in)) {
            System.out.printf("Unesite indeks studenta: ");
            Integer indeks = ulaz.nextInt();
            
            readPolozeniPredmetiCriteria(indeks);
        }

        System.out.println("Zavrsetak rada\n");
        HibernateUtil.getSessionFactory().close();
    }

    public static void readPolozeniPredmetiCriteria(Integer indeks){
        System.out.println("---------------------------------");
        Session session = HibernateUtil.getSessionFactory().openSession();
        Transaction TR = null;
        
        try {
            TR = session.beginTransaction();
            
            Student trazeniStudent = session.load(Student.class, indeks);
            String ime = trazeniStudent.getIme();
            String prezime = trazeniStudent.getPrezime();
            System.out.println("Student: " + indeks + " " + ime + " " + prezime);
            
            /* Postavljanje kriterijuma tako da kriterijume ispunjavaju polozeni
             * ispiti studenta sa brojem indeksa indeks */
            CriteriaBuilder cb = session.getCriteriaBuilder();
            
            CriteriaQuery<Ispit> criteria = cb.createQuery(Ispit.class);
            Root<Ispit> ispit = criteria.from(Ispit.class);
            
            Join<Ispit, Student> student = ispit.join("student");
            Join<Ispit, Predmet> predmet = ispit.join("predmet");
            
            criteria.select(ispit);
            criteria.where(cb.and(
                    cb.gt(ispit.get("ocena"), new Integer(5)), 
                    cb.like(ispit.get("status"), "o"), 
                    cb.equal(student.get("indeks"), indeks)
                    )
            );
            criteria.orderBy(cb.asc(predmet.get("naziv")));
            
            List<Ispit> results = session.createQuery(criteria).getResultList();
            results.stream()
                .map(i -> i.getPredmet().getNaziv())
                .forEach(naziv -> System.out.println("Ispit: " + naziv));
            
            TR.commit();            
        } catch (Exception e) {
            System.err.println("Postoji problem sa ispisivanjem informacija o studentima! Ponistavanje transakcije!");
        
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }
}

12.7 Zadaci za vežbu

Zadatak 12.3: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate i JPA Criteria API-ja izdvaja sve podatke o studentima koji su upisali fakultet godine koja se unosi sa standardnog ulaza.

Zadatak 12.4: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate i JPA Criteria API-ja za svaki predmet izdvaja oznaku i naziv, a zatim i spisak uslovnih predmeta. Za svaki uslovni predmet izdvojiti oznaku, naziv i broj ESPB bodova.

Zadatak 12.5: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate i JPA Criteria API-ja za studenta, čiji se indeks unosi sa standardnog ulaza, pravi izveštaj o položenim ispitima i uspehu po upisanim školskim godinama. Za svaku upisanu godinu ispisati naziv predmeta, datum polaganja i ocenu, a zatim prosečnu ocenu.