Capitolo 11. HQL: Il linguaggio di interrogazione di Hibernate (Hibernate Query Language)

Hibernate è dotato di un linguaggio di interrogazione estremamente potente che (del tutto intenzionalmente) assomiglia molto all'SQL. Ma la sintassi non deve ingannare: l'HQL è pienamente orientato agli oggetti, e comprende nozioni come l'ereditarietà, il polimorfismo e l'associazione.

11.1. Dipendenza da maiuscole e minuscole

Le interrogazioni non distinguono tra maiuscole e minuscole, eccetto per i nomi delle classi java e delle proprietà. Quindi SeLeCT è la stessa cosa di sELEct che è la stessa cosa di SELECT ma net.sf.hibernate.eg.FOO non è net.sf.hibernate.eg.Foo e foo.barSet non è foo.BARSET.

Questo manuale fa uso di parole chiave HQL in lettere minuscole. Alcuni utenti trovano che le interrogazioni con parole chiave in maiuscolo siano più leggibili, ma troviamo che questa convenzione sia brutta, quando utilizzata in interrogazioni annidate in codice java.

11.2. La clausola from

L'interrogazione più semplice possibile in Hibernate ha la forma:

from eg.Cat

che restituisce semplicemente tutte le istanze della classe eg.Cat.

La maggior parte delle volte, avrete bisogno di assegnare un sinonimo, poiché vorrete fare riferimento al Cat in altre parti dell'interrogazione.

from eg.Cat as cat

Questa query assegna il sinonimo cat alle istanze di Cat, in modo da poter usare quel sinonimo più avanti nell'interrogazione. La parola chiave as è opzionale, potremmo anche scrivere:

from eg.Cat cat

Possono apparire anche classi multiple, il che risulta in un prodotto cartesiano o join "incrociato".

from Formula, Parameter
from Formula as form, Parameter as param

Viene considerata una buona abitudine dare ai sinonimi delle interrogazioni nomi che comincino con lettere minuscole, in maniera coerente con gli standard di denominazione di Java per le variabili locali (ad esempio domesticCat).

11.3. Associazioni e join

Possiamo anche assegnare sinonimi ad entità associate, o anche ad elementi di una collezione di valori, usando un join.

from eg.Cat as cat 
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

from eg.Cat as cat left join cat.mate.kittens as kittens

from Formula form full join form.parameter param

I tipi di join supportati sono presi in prestito dall'SQL ANSI

  • inner join

  • left outer join

  • right outer join

  • full join (di solito inutile)

I costrutti inner join, left outer join e right outer join possono venire abbreviati.

from eg.Cat as cat 
    join cat.mate as mate
    left join cat.kittens as kitten

In aggiunta, un join di tipo "fetch" (raccolta) consente di inizializzare le associazioni o le collezioni insieme agli oggetti genitori, usando una singola select. Questo è particolarmente utile nel caso di una collezione. Sovrascrive in maniera efficace le dichiarazioni dei join esterni (outer join) e della raccolta differita (lazy) del file di mappaggio per le associazioni e le collezioni.

from eg.Cat as cat 
    inner join fetch cat.mate
    left join fetch cat.kittens

Un "fetch join" (join con raccolta) non ha solitamente bisogno di assegnare un sinonimo, perché gli oggetti associati non dovrebbero venire usati nella clausola where (né in un'altra clausola qualsiasi). Nello stesso modo, gli oggetti associati non vengono restituiti direttamente nei risultati della query. Possono, invece, essere raggiunti tramite l'oggetto genitore

Notate che, nell'implementazione corrente, solo un ruolo di collezione può essere concretizzato ("fetched") in una interrogazione (qualsiasi altra cosa non sarebbe performante). Notate anche che il costrutto fetch non può essere usato in interrogazioni chiamate usando scroll() o iterate(). Notate infine che full join fetch e right join fetch non hanno senso.

11.4. La clausola select

La clausola select sceglie quali oggetti e proprietà vanno restituiti nel set dei risultati della query. Considerate che:

select mate 
from eg.Cat as cat 
    inner join cat.mate as mate

La query selezionerà gli amici (mates) dei gatti (Cats). In realtà è possibile esprimere la stessa interrogazione in maniera più compatta come:

select cat.mate from eg.Cat cat

Potete anche selezionare elementi di una collezione, usando la funzione speciale elements. L'interrogazione seguente restituisce tutti i gattini (kittens) di ogni gatto (cat).

select elements(cat.kittens) from eg.Cat cat

Le interrogazioni possono restituire proprietà di qualsiasi tipo di valore, comprese le proprietà di tipo componente:

select cat.name from eg.DomesticCat cat
where cat.name like 'fri%'

select cust.name.firstName from Customer as cust

Le interrogazioni possono restituire oggetti multipli e/o proprietà come un array di tipo Object[]

select mother, offspr, mate.name 
from eg.DomesticCat as mother
    inner join mother.mate as mate
    left outer join mother.kittens as offspr

o come un oggetto java tipizzato

select new Family(mother, mate, offspr)
from eg.DomesticCat as mother
    join mother.mate as mate
    left join mother.kittens as offspr

purché ovviamente Family abbia un costruttore appropriato.

11.5. Funzioni aggregate

Le query HQL possono anche restituire i risultati di funzioni aggregate sulle proprietà:

select avg(cat.weight), sum(cat.weight), max(cat.weight), count(cat)
from eg.Cat cat

Le collezioni possono anche apparire all'interno di funzioni aggregate nella clausola select.

select cat, count( elements(cat.kittens) ) 
from eg.Cat cat group by cat

Le funzioni aggregate supportate sono

  • avg(...), sum(...), min(...), max(...)

  • count(*)

  • count(...), count(distinct ...), count(all...)

Le parole chiave distinct e all possono essere usate con la stessa semantica dell'SQL.

select distinct cat.name from eg.Cat cat

select count(distinct cat.name), count(cat) from eg.Cat cat

11.6. Interrogazioni polimorfiche

Una interrogazione come:

from eg.Cat as cat

non restituisce solo istanze di Cat, ma anche delle sottoclassi come DomesticCat. Le interrogazioni di Hibernate possono indicare qualsiasi classe o interfaccia Java nella clausola from. L'interrogazione restituirà istanze di tutte le classi persistenti che estendono quella classe o implementano l'interfaccia. La prossima interrogazione restituisce tutti gli oggetti persistenti:

from java.lang.Object o

L'interfaccia Named potrebbe essere implementata da diverse classi persistenti:

from eg.Named n, eg.Named m where n.name = m.name

Notate che queste ultime due interrogazioni richiederanno più di una SELECT SQL. Questo significa che la clausola order by non ordinerà correttamente l'intero insieme dei risultati. (e significa anche che non potete chiamare le query usando Query.scroll().)

11.7. La clausola where

La clausola where consente di limitare la lista di istanze rese da una interrogazione.

from eg.Cat as cat where cat.name='Fritz'

restituisce le istanze di Cat il cui nome (name) è 'Fritz'.

select foo 
from eg.Foo foo, eg.Bar bar
where foo.startDate = bar.date

restituirà tutte le istanze di Foo per le quali esiste una istanza di bar con una proprietà date uguale alla proprietà startDate del Foo. Le espressioni a percorso composto fanno sì che la clausola where sia estremamente potente. Considerate:

from eg.Cat cat where cat.mate.name is not null

Questa interrogazione si traduce in una query SQL con un join di tabella (interno) Se doveste scrivere una cosa come

from eg.Foo foo  
where foo.bar.baz.customer.address.city is not null

otterreste una query che richiederebbe quattro join di tabella in SQL.

L'operatore = può essere usato per confrontare non solo proprietà, ma anche istanze:

from eg.Cat cat, eg.Cat rival where cat.mate = rival.mate

select cat, mate 
from eg.Cat cat, eg.Cat mate
where cat.mate = mate

La proprietà speciale (in minuscolo) id può essere usata per fare riferimento all'identificatore univoco di un oggetto. (potete anche usare il suo nome di proprietà)

from eg.Cat as cat where cat.id = 123

from eg.Cat as cat where cat.mate.id = 69

La seconda query è efficiente. Non è richiesto un join di tabella!

Possono anche essere usate le proprietà di identificatori compositi. Supponete che Person abbia un identificatore composto che consiste in country e medicareNumber.

from bank.Person person
where person.id.country = 'AU' 
    and person.id.medicareNumber = 123456

from bank.Account account
where account.owner.id.country = 'AU' 
    and account.owner.id.medicareNumber = 123456

Ancora una volta, la seconda interrogazione non richiede join di tabella.

Nello stesso modo, la proprietà speciale class accede al valore del discriminatore di una istanza nel caso della persistenza polimorfica. Un nome di classe java annidato nella clausola where verrà tradotto nel suo valore di discriminazione.

from eg.Cat cat where cat.class = eg.DomesticCat

Potete anche specificare proprietà o componenti o tipi utente compositi (e di componenti di componenti, ecc.). Non tentate di utilizzare una espressione di percorso che finisca in una proprietà di tipo di componente (in opposizione ad una proprietà di un componente). Ad esempio, se store.owner è una entità con un componente indirizzo (address)

store.owner.address.city    // okay
store.owner.address         // error!

Un tipo "any" ha le proprietà speciali id e class, che consentono di esprimere un join nel modo seguente (in cui AuditLog.item è una proprietà mappata con <any>).

from eg.AuditLog log, eg.Payment payment 
where log.item.class = 'eg.Payment' and log.item.id = payment.id

Notate che log.item.class e payment.class possono fare riferimento ai valori di colonne di database completamente diverse nella query precedente.

11.8. Espressioni

Le espressioni consentite nella clausola where includono la maggior parte delle cose che si scriverebbero in SQL:

  • operatori matematici +, -, *, /

  • operatori di confronto binario =, >=, <=, <>, !=, like

  • operazioni logiche and, or, not

  • concatenamento di stringhe ||

  • funzioni scalari SQL come upper() e lower()

  • le parentesi ( ) indicano i raggruppamenti

  • in, between, is null

  • parametri di ingresso JDBC ?

  • parametri con nome :name, :start_date, :x1

  • letterali SQL 'foo', 69, '1970-01-01 10:00:01.0'

  • costanti Java public static final come eg.Color.TABBY

in e between possono essere usati così:

from eg.DomesticCat cat where cat.name between 'A' and 'B'

from eg.DomesticCat cat where cat.name in ( 'Foo', 'Bar', 'Baz' )

e le corrispondenti forme negative possono essere scritte

from eg.DomesticCat cat where cat.name not between 'A' and 'B'

from eg.DomesticCat cat where cat.name not in ( 'Foo', 'Bar', 'Baz' )

Nello stesso modo, is null e is not null possono essere usati per testare i valori null.

I booleani possono essere utilizzati facilmente nelle espressioni dichiarando delle sostituzioni HQL nella configurazione di hibernate:

<property name="hibernate.query.substitutions">true 1, false 0</property>

Questo sostituirà le parole chiave true e false con i letterali 1 and 0 nell'SQL tradotto da questo HQL:

from eg.Cat cat where cat.alive = true

Potete controllare la dimensione di una collezione con la proprietà speciale size, o la funzione speciale size().

from eg.Cat cat where cat.kittens.size > 0

from eg.Cat cat where size(cat.kittens) > 0

Per le collezioni indicizzate, potete fare riferimento agli indici minimo e massimo usando minIndex e maxIndex. Nello stesso modo, potete fare riferimento agli elementi minimo e massimo di una collezione di un tipo di base usando minElement e maxElement.

from Calendar cal where cal.holidays.maxElement > current date

Ci sono anche le forme funzionali (le quali, a differenza dei costrutti qui sopra, non sono sensibili a maiuscole e minuscole):

from Order order where maxindex(order.items) > 100

from Order order where minelement(order.items) > 10000

Le funzioni SQL any, some, all, exists, in sono supportate quando viene loro passato l'insieme degli elementi o degli indici di una collezione (con le funzioni elements e indices) o il risultato di una sotto-interrogazione (vedete oltre).

select mother from eg.Cat as mother, eg.Cat as kit
where kit in elements(foo.kittens)

select p from eg.NameList list, eg.Person p
where p.name = some elements(list.names)

from eg.Cat cat where exists elements(cat.kittens)

from eg.Player p where 3 > all elements(p.scores)

from eg.Show show where 'fizard' in indices(show.acts)

Notate che questi costrutti - size, elements, indices, minIndex, maxIndex, minElement, maxElement - hanno alcune restrizioni di utilizzo:

  • in una clausola where: solo per database con subselect

  • in una clausola select: solo elements e indices hanno senso

Gli elementi delle collezioni indicizzate (array, liste, mappe) possono essere reperiti tramite il loro indice (solo in una clausola where):

from Order order where order.items[0].id = 1234

select person from Person person, Calendar calendar
where calendar.holidays['national day'] = person.birthDay
    and person.nationality.calendar = calendar

select item from Item item, Order order
where order.items[ order.deliveredItemIndices[0] ] = item and order.id = 11

select item from Item item, Order order
where order.items[ maxindex(order.items) ] = item and order.id = 11

Le espressioni all'interno di [] possono anche essere espressioni matematiche.

select item from Item item, Order order
where order.items[ size(order.items) - 1 ] = item

L'HQL fornisce anche la funzione predefinita index(), per gli elementi di una associazione uno-a-molti o una collezione di valori.

select item, index(item) from Order order 
    join order.items item
where index(item) < 5

Possono essere usate le funzioni scalari SQL supportate dal database sottostante

from eg.DomesticCat cat where upper(cat.name) like 'FRI%'

Se non siete ancora convinti da tutto questo, pensate a quanto più lunga e meno leggibile sarebbe la query seguente se dovesse essere espressa in SQL:

select cust
from Product prod,
    Store store
    inner join store.customers cust
where prod.name = 'widget'
    and store.location.name in ( 'Melbourne', 'Sydney' )
    and prod = all elements(cust.currentOrder.lineItems)

Suggerimento: qualcosa come

SELECT cust.name, cust.address, cust.phone, cust.id, cust.current_order
FROM customers cust,
    stores store,
    locations loc,
    store_customers sc,
    product prod
WHERE prod.name = 'widget'
    AND store.loc_id = loc.id
    AND loc.name IN ( 'Melbourne', 'Sydney' )
    AND sc.store_id = store.id
    AND sc.cust_id = cust.id
    AND prod.id = ALL(
        SELECT item.prod_id
        FROM line_items item, orders o
        WHERE item.order_id = o.id
            AND cust.current_order = o.id
    )

11.9. La clausola order by

La lista restituita da una query può essere ordinata secondo una qualsiasi proprietà di una delle classi restituite o dei componenti:

from eg.DomesticCat cat
order by cat.name asc, cat.weight desc, cat.birthdate

Gli elementi opzionali asc o desc indicano rispettivamente ordine ascendente o discendente.

11.10. La clausola group by

Una interrogazione che renda valori aggregati può essere raggruppata in base a una proprietà qualunque di una delle classi rese o dei componenti:

select cat.color, sum(cat.weight), count(cat) 
from eg.Cat cat
group by cat.color

select foo.id, avg( elements(foo.names) ), max( indices(foo.names) ) 
from eg.Foo foo
group by foo.id

Nota: potete usare i costrutti elements e indices in una clausola select, anche su database senza sub-select.

È consentita anche la clausola having.

select cat.color, sum(cat.weight), count(cat) 
from eg.Cat cat
group by cat.color 
having cat.color in (eg.Color.TABBY, eg.Color.BLACK)

Le funzioni SQL e le funzioni aggregate sono consentite nelle clausole having e order by, se supportate dal database sottostante (ad esempio non in MySQL).

select cat
from eg.Cat cat
    join cat.kittens kitten
group by cat
having avg(kitten.weight) > 100
order by count(kitten) asc, sum(kitten.weight) desc

Notate che né la clausola group by né la order by possono contenere espressioni aritmetiche.

11.11. Sottointerrogazioni

Per i database che supportano i sub-select, Hibernate supporta le sottointerrogazioni all'interno delle interrogazioni. Una sottointerrogazione deve essere circondata da parentesi (spesso da una chiamata di funzione aggregata SQL). Sono permesse anche le sottointerrogazioni correlate (ovvero quelle che fanno riferimento ad un sinonimo nella interrogazione esterna).

from eg.Cat as fatcat 
where fatcat.weight > ( 
    select avg(cat.weight) from eg.DomesticCat cat 
)

from eg.DomesticCat as cat 
where cat.name = some ( 
    select name.nickName from eg.Name as name 
)
    
from eg.Cat as cat 
where not exists ( 
    from eg.Cat as mate where mate.mate = cat 
)

from eg.DomesticCat as cat 
where cat.name not in ( 
    select name.nickName from eg.Name as name 
)

11.12. Esempi HQL

Le interrogazioni di Hibernate possono essere abbastanza potenti e complesse. In effetti, il potere del linguaggio di interrogazione è uno dei principali punti di forza di Hibernate. Qui presentiamo alcuni esempi di interrogazioni molto simili a query che sono state usate in un recente procetto. Notate che molte delle interrogazioni che scriverete sono molto più semplici di queste!

La prossima interrogazione restituisce l'id dell'ordine, il numero di oggetti e il valore totale dell'ordine per tutti gli ordini non pagati per un cliente particolare e un valore totale minimo, ordinando i risultati per valore totale. Nella determinazione dei prezzi, utilizza il catalogo corrente. La query SQL risultante, ha quattro join interni e una subselect non correlata che insistono sulle tabelle ORDER, ORDER_LINE, PRODUCT, CATALOG e PRICE.

select order.id, sum(price.amount), count(item)
from Order as order
    join order.lineItems as item
    join item.product as product,
    Catalog as catalog
    join catalog.prices as price
where order.paid = false
    and order.customer = :customer
    and price.product = product
    and catalog.effectiveDate < sysdate
    and catalog.effectiveDate >= all (
        select cat.effectiveDate 
        from Catalog as cat
        where cat.effectiveDate < sysdate
    )
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc

Che mostro! A dire il vero, nella vita reale non ho molta passione per le sottointerrogazioni, quindi la mia era più come la seguente:

select order.id, sum(price.amount), count(item)
from Order as order
    join order.lineItems as item
    join item.product as product,
    Catalog as catalog
    join catalog.prices as price
where order.paid = false
    and order.customer = :customer
    and price.product = product
    and catalog = :currentCatalog
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc

La prossima interrogazione conta il numero di pagamenti in ogni stato, escludendo tutti i pagamenti nello stato AWAITING_APPROVAL quando il cambiamento di stato più recente era stato fatto dall'utente corrente. Si traduce in una query SLQ con due join interni e una subselect correlata sulle tabelle PAYMENT, PAYMENT_STATUS e PAYMENT_STATUS_CHANGE.

select count(payment), status.name 
from Payment as payment 
    join payment.currentStatus as status
    join payment.statusChanges as statusChange
where payment.status.name <> PaymentStatus.AWAITING_APPROVAL
    or (
        statusChange.timeStamp = ( 
            select max(change.timeStamp) 
            from PaymentStatusChange change 
            where change.payment = payment
        )
        and statusChange.user <> :currentUser
    )
group by status.name, status.sortOrder
order by status.sortOrder

Se avessi mappato la collezione statusChanges come una lista invece di un set, l'interrogazione sarebbe stata molto più semplice da scrivere.

select count(payment), status.name 
from Payment as payment
    join payment.currentStatus as status
where payment.status.name <> PaymentStatus.AWAITING_APPROVAL
    or payment.statusChanges[ maxIndex(payment.statusChanges) ].user <> :currentUser
group by status.name, status.sortOrder
order by status.sortOrder

La prossima interrogazione usa la funzione isNull() di MS SQL Server per restituire tutti i conti e i pagamenti non effettuati per l'organizzazione a cui l'utente corrente appartiene. Si traduce in una query SQL con tre join interni, un join esterno e una subselect sulle tabelle ACCOUNT, PAYMENT, PAYMENT_STATUS, ACCOUNT_TYPE, ORGANIZATION e ORG_USER.

select account, payment
from Account as account
    left outer join account.payments as payment
where :currentUser in elements(account.holder.users)
    and PaymentStatus.UNPAID = isNull(payment.currentStatus.name, PaymentStatus.UNPAID)
order by account.type.sortOrder, account.accountNumber, payment.dueDate

Per alcuni database, avremmo bisogno di fare a meno della subselect correlata.

select account, payment
from Account as account
    join account.holder.users as user
    left outer join account.payments as payment
where :currentUser = user
    and PaymentStatus.UNPAID = isNull(payment.currentStatus.name, PaymentStatus.UNPAID)
order by account.type.sortOrder, account.accountNumber, payment.dueDate

11.13. Suggerimenti

Potete contare il numero dei risultati di una interrogazione senza restituirli veramente:

( (Integer) session.iterate("select count(*) from ....").next() ).intValue()

Per ordinare un risultato per dimensione di una collezione, usate l'interrogazione seguente:

select usr.id, usr.name
from User as usr 
    left join usr.messages as msg
group by usr.id, usr.name
order by count(msg)

Se il vostro database supporta le sottointerrogazioni, potete mettere una condizione sulla dimensione della selezione nella clausola where della vostra query:

from User usr where size(usr.messages) >= 1

Mentre se il database non supporta i subselect potete usare la query seguente:

select usr.id, usr.name
from User usr.name
    join usr.messages msg
group by usr.id, usr.name
having count(msg) >= 1

Poiché questa soluzione non può restituire uno User con zero messaggi a causa del join interno, è anche utile la forma seguente:

select usr.id, usr.name
from User as usr
    left join usr.messages as msg
group by usr.id, usr.name
having count(msg) = 0

Le proprietà di un javabean possono essere assegnate a parametri della query con nome:

Query q = s.createQuery("from foo in class Foo where foo.name=:name and foo.size=:size");
q.setProperties(fooBean); // fooBean has getName() and getSize()
List foos = q.list();

Le collezioni sono paginabili usando l'interfaccia Query con un filtro:

Query q = s.createFilter( collection, "" ); // the trivial filter
q.setMaxResults(PAGE_SIZE);
q.setFirstResult(PAGE_SIZE * pageNumber);
List page = q.list();

Gli elementi delle collezioni possono essere ordinati o raggruppati usando un filtro di interrogazione:

Collection orderedCollection = s.filter( collection, "order by this.amount" );
Collection counts = s.filter( collection, "select this.type, count(this) group by this.type" );

Potete individuare la dimensione di una collezione senza inizializzarla:

( (Integer) session.iterate("select count(*) from ....").next() ).intValue();