Untitled Document
   
 
Usare JDO 2.0: JDOQL - Parte 2
di Robin Roos, traduzione di Lorenzo Felici

Premessa
Benvenuti alla seconda delle quattro parti di cui è formata questa mia serie su JDOQL. Gli articoli in questa serie illustrano le nuove potenzialità che l'imminente standard JDO 2.0 fornirà a JDOQL.
Questa prima parte della serie comprende:

  • JDOQL Parte I - Nuovi operatori e metodi supportati, il paging dei risultati delle query e query di cancellazione delegate al dB.
  • JDOQL Parte II - Proiezione, aggregazione e raggruppamento senza duplicazione dei risultati delle query.

Questo particolare articolo utilizza per il dominio il modello ad oggetti, discusso separatamente, e l'insieme di dati per Hammer.org in Online Auction Domain. Qui viene fornito un veloce link al diagramma UML per il dominio persistente.

Altre utili risorse includono due pagine di SolarMetric JDOQL 2.0 Quick Reference e la PDF Edition non stampabile del mio libro, Java Data Objects (Addison-Wesley).

 

Selezione di offerte vincenti come dati e non come oggetti - La Proiezione
In JDO 1.0 tutte le query restituiscono collezioni d'istanze persistenti. Per esempio, una query su un'estensione dell'AuctionItem (oggetto d'asta) restituirebbe una collezione d'istanze di AuctionItem che soddisfano le condizioni del filtro. Nella maggior parte dei casi questo è ciò che si desidera. JDO gestisce la corrispondenza fra gli oggetti in memoria e la loro rappresentazione nella base di dati; l'applicazione disporrà quindi sia di oggetti reali che del loro comportamento.
Tuttavia, in determinate circostanze l'applicazione non è interessata agli oggetti reali. Potrebbe essere che tutto ciò che si desidera sia l'accesso ai dati persistenti e quindi una struttura di dati semplice potrebbe essere preferibile ad un oggetto da cui quegli stessi dati devono essere poi estrapolati.
Si consideri la richiesta di selezionare tutti gli AuctionItem per cui l'offerta vincente è più alta del prezzo di base e quindi di estrarre il titolo, il prezzo di riserva, l'importo dell'offerta vincente e il nome dell'offerente per mostrarlo all'utente.(facendo uso di JSP, o Swing, ecc).


JDOQL (#17) senza proiezione
Ecco come potrebbe essere realizzato ciò, senza l'uso della nuova caratteristica della proiezione di JDO 2.0:


Query q = pm.newQuery(AuctionItem.class, "winningBid.bidAmount >
reservePrice");
Collection results = (Collection) q.execute ();
Collection data = new ArrayList();
Iterator iter = results.iterator();
while (iter.hasNext()) {
AuctionItem item = (AuctionItem) iter.next();
Object[] row = new Object[4];
row[0] = item.getTitle();
row[1] = item.getReservePrice();
row[2] = item.getWinningBid().getBidAmount();
row[3] = item.getWinningBid().getBidder().getUser().getUserName();
data.add(row);
}


Alla fine dell'iterazione l'applicazione ha ottenuto una Collection di array Object[] di lunghezza 4, ciascuno, appunto, con quattro dati recuperati dall'appropriato AuctionItem e dagli oggetti che con esso correlati. In ogni modo ottenere tale risultato è complicato dal punto di vista dell'implementazione e non sufficientemente efficiente ai fini dell'esecuzione.


JDOQL (#18) con proiezione
JDO 2.0 soddisfa la richiesta in oggetto supportando la proiezione, che è rappresentata dalla clausola "result" di una Query. La clausola result è una lista di campi separati dalla virgola, che potrebbero essere campi persistenti della classe candidata oppure campi d'altre classi raggiungibile dalla classe candidata. La parola chiave "this" può essere usata per riferirsi all'istanza stessa che soddisfa la richiesta. L'accesso agli ID degli oggetti persistenti è fornito con il metodo JDOHelper.getObjectId().

Query q = pm.newQuery (AuctionItem.class, "winningBid.bidAmount >
reservePrice");
q.setResult ("title, reservePrice, winningBid.bidAmount,
winningBid.bidder.user.userName");
Collection results = (Collection)q.execute ();

Risultati
I risultati dei due frammenti di codice sono identici, sia nella struttura che nei contenuti, ma la versione che utilizza la clausola result per la proiezione è molto più semplice da scrivere e può essere più facilmente implementata in modo efficiente.


 

Proiezione o no?
JDO è una tecnologia ORM (Object-Relational Mapping). Come tale permette un maggior livello di astrazione, sollevando gli sviluppatori da gran parte del lavoro necessario alla composizione degli oggetti a partire dai campi e al processo inverso di decomposizione degli oggetti nuovamente dentro i campi. La proiezione permette agli utenti di evitare questo lavoro.
Ogni campo proiettato può essere di tipo semplice (dati) o di tipo capace di persistenza (oggetti). Quando un oggetto di quest'ultimo tipo è parte di una proiezione, JDO continua a gestire l'oggetto proiettato. L'esempio successivo presenta una proiezione che include un oggetto RegisteredUser anziché solo il valore del campo del nome dell'oggetto stesso.

 

Id degli Oggetti
E' molto comodo restituire una struttura tabellare di dati per un client o per visualizzarli tramite una JSP. Tuttavia, alle volte, il client può richiedere di conoscere l'ID dell'oggetto per ogni riga, in modo che l'ID corretto possa essere riutilizzato per richieste successive. Per esempio, un client Swing potrebbe visualizzare la JTable degli articoli dell'asta, permettendo all'utente di selezionarne uno per un'interrogazione più dettagliata. Quest'ultima interrogazione può essere soddisfatta più efficientemente se l'applicazione client rinvia l'ID dell'oggetto. Per facilitare ciò l'ID dell'oggetto deve fare parte della struttura dati che il client riceve.

q.setResult ("title, reservePrice, winningBid.bidAmount,
             winningBid.bidder.user");

Alla luce di ciò: quando si dovrebbe proiettare i dati a livello di campi e quando si dovrebbe proiettare tipi di oggetti capaci di persistenza?

Si DEVE proiettare dati a livello di campo quando:

  • i dati saranno usati, ma non saranno modificati, tipicamente quando i valori devono essere trasmessi fuori dallo strato di JDO per sola lettura (per sola visualizzazione).
    NON SI DEVE proiettare dati a livello di campo quando:
  • si dovrebbero creare oggetti (in modo analogo alle classi capaci di persistenza) a partire dai campi; i tal caso si consideri il "detachment" anziché la proiezione.
  • si devono modificare i dati e avere tali modifiche sincronizzate nella base di dati; in tal caso si progettino invece oggetti capaci di persistenza e si lasci a JDO il compito di svolgere il lavoro più arduo .
  • se intendete semplicemente imporre dei pattern di caricamento per le query; in tal caso si usi piuttosto la funzionalità di JDO 2.0 di pianificazione del caricamento.

 

JDOQL (#19)
Gli ID degli oggetti possono essere inclusi nei risultati proiettati usando il metodo JDOHelper.getObjectId() nella clausola result, come illustrato dall'esempio seguente.

Query q = pm.newQuery (AuctionItem.class,
                       "winningBid.bidAmount > reservePrice");
q.setResult ("JDOHelper.getObjectId(this), title,
              reservePrice, winningBid.bidAmount,
              winningBid.bidder.user.userName");
Collection results = (Collection)q.execute ();

 

Risultati
I risultati ora includono l'ID dell'oggetto per ogni articolo selezionato dell'asta.

SQL
Per riferimento, ecco l'SQL che è stato generato dalla query proiettata includendo l'ID dell'oggetto:

SELECT t0.JDOID, t0.TITLE, t0.RESERVEPRICE, t1.BIDAMOUNT, t3.USERNAME
FROM AUCTIONITEM t0 INNER JOIN BID t1 ON t0.WINNINGBID_JDOID = t1.JDOID
LEFT OUTER JOIN BIDDER t2 ON t1.BIDDER_JDOID = t2.JDOID LEFT OUTER JOIN
REGISTEREDUSER t3 ON t2.USER_JDOID = t3.JDOID
WHERE (t1.BIDAMOUNT > t0.RESERVEPRICE)

 

Proiezione di singoli campi
Ci sono due casi speciali di proiezione degni di essere qui illustrati. Richiedono entrambi le proiezioni d'espressioni con un singolo campo. Nel caso in cui la proiezione contiene soltanto un campo, non è necessario l'utilizzo di un contenitore Object[]. I risultati della query sono poi una collezione di tipi del campo (o del tipo di wrapper corrispondente) o, se la proprietà unique della query ha valore "vero", una singola istanza del tipo del campo (o del tipo di wrapper corrispondente).
Questi casi possono essere meglio illustrati mediante due esempi.

 

JDOQL (#20) per la proiezione di un campo singolo unique
Il primo esempio seleziona soltanto gli indirizzi email degli utenti registrati. Il singolo campo è di tipo String, ma poiché la query non è unica il risultato è una collezione di oggetti di tipo String.

Query q = pm.newQuery (RegisteredUser.class);
q.setResult ("emailAddress");
Collection results = (Collection)q.execute ();
// l'esecuzione ritorna una collezione
Iterator iter = results.iterator();
while (iter.hasNext()) {
String email = (String) iter.next();
// la collezione contiene tipi String e non Object[]
// altra implementazione richiesta
}

Risultato


JDOQL (#21) per la proiezione di un campo singolo unique
La seconda query seleziona l'indirizzo e-mail di uno specifico utente. Qui è stata esplicitamente posta uguale a "vero" la proprietà unique della query, poiché ci si aspetta che vi sia al più una sola istanza che soddisfa il filtro. Il risultato è quindi un singolo oggetto di tipo String, senza contenitore al contorno.


Query q = pm.newQuery (RegisteredUser.class, "userName == 'Sophie'");
q.setResult ("emailAddress");
q.setUnique(true);
String email = (String) q.execute();
// esecuzione ritorna un singolo oggetto di tipo String

 

Risultato

 

Selezione di dati distinct per le offerte vincenti
Quando una query JDOQL restituisce le istanze persistenti della classe candidata non vi è alcuna ragione di rimuovere i duplicati; l'unicità delle istanze nella collezione restituita è garantita da JDO.
Tuttavia, in una query che fa uso della proiezione (elencando uno o più campi nella clausola result), è facile che il risultato contenga duplicati.
Si consideri la seguente query che seleziona solo il nome dell'offerente che ha fatto l'offerta vincente per ogni articolo in cui il prezzo di riserva è stato superato:


JDOQL (#22) senza distinct - kodo workbench screenshot

Query q = pm.newQuery (AuctionItem.class);
q.setResult ("winningBid.bidder.user.userName");
Collection results = (Collection)q.execute ();


Jaques e Sophie hanno entrambi piazzato offerte vincente per più di un articolo.
A seconda del particolare caso d'uso dell'applicazione può essere preferibile avere risultati distinti. Rendere non duplicabili tali dati non può essere realizzato in modo efficiente sul client e, qualora si abbia a che fare con insiemi di dati di grandi dimensione, potrebbe risultare al quanto impraticabile.
Per risolvere questo problema JDO 2.0 ha aggiunto il supporto della parola chiave distinct, legittima solo come prima parola nella clausola result della query. Questa possibilità è tradotta in un'istruzione SQL valida per il database sottostante al fine di rimuovere i duplicati nel modo più efficiente possibile.

JDOQL (#23) usando distinct
Ecco la query rivista:

Query q = pm.newQuery (AuctionItem.class);
q.setResult ("distinct winningBid.bidder.user.userName");
Collection results = (Collection)q.execute ();

E i risultati:


 

SQL
Infine ecco l'SQL che è stato generato:
SELECT DISTINCT t3.USERNAME
FROM AUCTIONITEM t0 LEFT OUTER JOIN BID t1 ON t0.WINNINGBID_JDOID =
t1.JDOID LEFT OUTER JOIN BIDDER t2 ON t1.BIDDER_JDOID = t2.JDOID
LEFT OUTER JOIN REGISTEREDUSER t3 ON t2.USER_JDOID = t3.JDOID

 

Selezione di offerte vincenti con JavaBeans dedicati - Result class
Senza proiezione una query JDOQL restituisce sempre una o più istanze della classe candidata. La proiezione permette a chi scrive la query di identificare una lista di campi e di espressioni sui campi d'interesse; quindi il risultato dell'esecuzione della query è formato da uno o più Object[], ciascuno dei quali contiene il valore del campo selezionato corrispondente ad una delle istanze che soddisfa il criterio del filtro.
A volte, tuttavia, non è desiderabile ottenere come risultato un Object[] per ogni istanza che soddisfa il criterio del filtro. Lavorare con Object[] può risultare difficoltoso, e una classe JavaBeans può essere più appropriata. In questo contesto un JavaBeans è una classe con un costruttore pubblico senza parametri e con metodi get/set per ogni sua proprietà.
JDO 2.0 permette di specificare nelle query il descrittore di una classe JavaBeans che dovrà contenere il risultato della query. Questa è conosciuta come "result class". I campi delle istanze che soddisfano il criterio della query sono in corrispondenza con proprietà di questa classe, un'istanza di JavaBeans per ogni istanza persistente della classe candidata che soddisfa il criterio della query. La corrispondenza di default tra i campi della proiezione e le proprietà del JavaBeans è basata sul nome stesso del campo, ma se necessario l'autore della query può specificare un'alias per ogni campo proiettato. L'alias specificato viene poi usato per determinare la proprietà corrispondente del JavaBeans.
Tecnicamente la result class può essere più semplice di un vero e proprio JavaBeans; JDO non richiede metodi accessori (get) per le proprietà ed è legittimo usare campi pubblici anziché proprietà.

 

Proiezione senza result class
Qui di seguito viene riportata una query d'esempio, tratta da una precedente sezione, che proietta una coppia di campi e di espressioni del campo raggiungibili per gli articoli dell'asta nei quali i prezzi di riserva sono stati superati.


Query q = pm.newQuery (AuctionItem.class,
"winningBid.bidAmount > reservePrice");
q.setResult ("title, reservePrice, winningBid.bidAmount,
winningBid.bidder.user.userName");
Collection results = (Collection)q.execute ();
Iterator iter = results.iterator();
while (iter.hasNext()) {
Object[] row = (Object[]) iter.next();
// additional processing as required
}

Risultati
Il risultato di questa query è una collezione di Object[]. Per l'insieme di dati considerati, i risultati erano:


 

Definizione della result class JavaBeans
Ecco qui il codice sorgente di un JavaBeans realizzato per contenere le stesse informazioni.

public class WinningBidBean {
private String title;
private double reservePrice;
private double bestBid;
private String userName;
public String getTitle() {return title;}
public double getReservePrice()
{return reservePrice;}
public double getBestBid() {return bestBid;}
public String getUserName() {return userName;}
public void setTitle(String title)
{this.title = title;}
public void setReservePrice(double reservePrice)
{this.reservePrice = reservePrice;}
public void setBestBid(double bestBid)
{this.bestBid = bestBid;}
public void setUserName(String userName)
{this.userName = userName;}
}


Il costruttore di default è pubblico e senza argomenti.

 

Proiezione con la result class
Ora che abbiamo la classe JavaBeans è facile far popolare alla query le istanze di WinningBidBean anziché Object[]. A tal fine si deve inizializzare la result class della query. E' anche necessario creare un'alias per l'importo dell'offerta vincente, che deve corrispondere alla proprietà "bestBid" del bean. Anche il campo winningBid.bidder.user.userName necessita di un'alias poiché non è un campo della classe candidata, perciò gli è stato esplicitamente assegnato l'alias "userName".


JDOQL (#24)
Qui di seguito viene riportata la query che ne risulta.

Query q = pm.newQuery (AuctionItem.class,
"winningBid.bidAmount > reservePrice");
q.setResult ("title, reservePrice, winningBid.bidAmount
as bestBid, winningBid.bidder.user.userName as userName");
q.setResultClass(WinningBidBean.class);
Collection results = (Collection)q.execute ();
Iterator iter = results.iterator();
while (iter.hasNext()) {
WinningBidBean row = (WinningBidBean) iter.next();
// additional processing as required
}

 

Risultati
Naturalmente la quey selezionerà e restituirà gli stessi valori. Tutta la differenza sta nel tipo dell'elemento che contiene i valori restituiti. In questo caso il risultato viene ritornato all'interno di una collezione d'istanze di WinningBidBean.

 

Proiezione in una concreta implementazione di Map
Su questo tema vi è una variazione finale degna di nota. Invece di indicare una classe JavaBeans come "result Class" di una query, si può specificare una classe concreta che implementa l'interfaccia java.util.Map. Ogni campo proiettato dalla query viene copiato dentro l'oggetto map in corrispondenza di un campo chiave che contiene il nome del campo stesso (o l'alias).

 

JDOQL (#25)
Questo esempio proietta gli stessi dati dentro una Map ed illustra le chiavi in corrispondenza delle quali risiedono i valori di ciascun campo.


Query q = pm.newQuery (AuctionItem.class,
"winningBid.bidAmount > reservePrice");
q.setResult ("title, reservePrice, winningBid.bidAmount
as bestBid, winningBid.bidder.user.userName as userName");
q.setResultClass(HashMap.class);
Collection results = (Collection)q.execute ();
Iterator iter = results.iterator();
while (iter.hasNext()) {
Map row = (Map) iter.next();
System.out.println(row.get("title"));
System.out.println(row.get("reservePrice"));
System.out.println(row.get("bestBid"));
System.out.println(row.get("userName"));
}

 

Riepilogo dei dati dell'Asta - Aggregazione
Storicamente JDOQL era focalizzato sul recupero dal database di istanze di classi persistenti. Il risultato restituito dall'esecuzione della query era una collezione d'istanze che soddisfavano la condizione del filtro.
L'aggiunta della proiezione a JDOQL ha permesso di scrivere query che recuperano strutture di dati anziché istanze, mediante la specificazione di una clausola result della query, clausola che elenca campi ed espressioni dei campi delimitati da virgole. Mediante l'esecuzione di una tale query vengono restituiti uno o più Object[] o istanze di JavaBeans.
Un'altra nuova caratteristica di JDO 2.0 è l'aggiunta delle funzioni aggregate al linguaggio di query. Qui di seguito vengono riportate le funzioni aggregate supportate e i loro tipi Java:


La clausola result della query può contenere espressioni di aggregazione in aggiunta ai campi e alle espressioni dei campi. Ogni query che usa le espressioni di aggregazione e non ha una clausola di raggruppamento (si veda dopo) implicitamente restituisce al più una riga, così non è necessario invocare esplicitamente il setUnique(true).

 

JDOQL (#26)
Ecco la query nel quale sono illustrati le funzioni d'aggregazione.

Query q = pm.newQuery (AuctionItem.class);
q.setResult ("count(this), min(reservePrice), max(reservePrice),
avg(reservePrice), sum(reservePrice)");
// unique=true implicitly since the query result includes
aggregazione senza raggruppamento
// Object[] ritornato non è contenuto nella collezione.
Object[] result = (Object[]) q.execute ();

 

Risultato
Il campo reservePrice è di tipo double, quindi le funzioni aggregate applicate ad esso restituiscono istanze di Double. Count restituisce Long.

SQL
SELECT COUNT(t0.JDOID), MIN(t0.RESERVEPRICE), MAX(t0.RESERVEPRICE),
AVG(t0.RESERVEPRICE), SUM(t0.RESERVEPRICE)
FROM AUCTIONITEM t0


Query unique con singolo aggregato
Se una query JDOQL è configurata come unique (l'autore si aspetta non più di una riga che soddisfa la condizione) allora il risultato della query non è "confezionato" in una collezione. Inoltre, se la clausola result della query contiene solo una singola espressione, allora il valore risultante non è "confezionato" in un Object[]. Infine, se la clausola result di una query contiene soltanto espressioni aggregate, allora la query è implicitamente unica. Così il caso semplice diventa veramente molto semplice.
Qui sotto è riportata la query che seleziona l'esposizione totale di un utente specifico. Questa è la somma di tutte le offerte che hanno piazzato e che attualmente sono le offerte vincenti.


JDOQL (#27)

Query q = pm.newQuery(Bid.class, "bidder.user.userName==name && this == item.winningBid");
q.setResult ("sum(bidAmount)");
q.declareParameters ("String name");
Double result = (Double) q.execute("Sophie");

Risultato
Si noti che l'oggetto restituito dall'esecuzione della query è un Double. Non è necessario utilizzare collezioni e Object[] per estrarre il risultato voluto.

SQL
Ecco l'SQL che è stato eseguito rispetto allo schema qui considerato per la base di dati:

SELECT SUM(t0.BIDAMOUNT)
FROM BID t0 INNER JOIN BIDDER t1 ON t0.BIDDER_JDOID =
t1.JDOID INNER JOIN REGISTEREDUSER t2 ON t1.USER_JDOID =
t2.JDOID INNER JOIN AUCTIONITEM t3 ON t0.ITEM_JDOID = t3.JDOID
WHERE (t2.USERNAME = 'Sophie' AND t0.JDOID = t3.WINNINGBID_JDOID)

Raggruppamento di Risultati Aggregati
Poter aggregare risultati è incredibilmente utile. Inoltre, con il raggruppamento l'aggregazione diventa ancor più potente.
Si consideri la richiesta d'individuare le maggiori offerte che un utente specifico ha piazzato. Mediante il solo utilizzo dell'aggregazione possiamo stabilire che la più grande offerta piazzata da Sophie è pari a 18,75.


JDOQL (#28)

Query q = pm.newQuery (Bid.class, "bidder.user.userName == name");
q.setResult ("max(bidAmount)");
q.declareParameters ("String name");
Double result = (Double) q.execute ("Sophie");

SQL

SELECT MAX(t0.BIDAMOUNT)
FROM BID t0 INNER JOIN BIDDER t1 ON t0.BIDDER_JDOID = t1.JDOID
INNER JOIN REGISTEREDUSER t2 ON t1.USER_JDOID = t2.JDOID
WHERE (t2.USERNAME = 'Sophie')
Ma non è forse vero che è certamente più utile conoscere la miglior offerta che Sophie ha fatto per ogni articolo dell'asta? Ciò può essere ottenuto raggruppando il max(bidAmount) articolo per articolo.

JDOQL (#29)

Query q = pm.newQuery (Bid.class, "bidder.user.userName==name");
q.setResult ("max(bidAmount), item.description");
q.setGrouping ("item");
q.declareParameters ("String name");
Collection results = (Collection) q.execute ("Sohpie");

Risultati

 

SQL
SELECT MAX(t0.BIDAMOUNT), t3.DESCRIPTION
FROM BID t0 INNER JOIN BIDDER t1 ON t0.BIDDER_JDOID = t1.JDOID INNER
JOIN REGISTEREDUSER t2 ON t1.USER_JDOID = t2.JDOID LEFT OUTER JOIN
AUCTIONITEM t3 ON t0.ITEM_JDOID = t3.JDOID
WHERE (t2.USERNAME = 'Sophie')
GROUP BY t0.ITEM_JDOID

Having
Per concludere, forse sono considerate significative soltanto le massime offerte superiori ad una soglia fissata. I dati aggregati non possono essere filtrati entro la clausola stessa del filtro, ma le condizioni che includono gli aggregati possono essere specificate nella clausola having.
L'esempio sotto raggruppa per le massime offerte per articolo fatte da Sophie e mostra soltanto quelle per cui l'offerta massima eccede 10.00.


JDOQL (#30)

Query q = pm.newQuery(Bid.class, "bidder.user.userName==name");
q.setResult ("max(bidAmount), item.description");
q.setGrouping ("item having max(bidAmount) > 10.00");
q.declareParameters ("String name");
Collection results = (Collection) q.execute("Sophie");

SQL
Ecco l'SQL per l'esempio finale. Si tenga a mente che, anche se non tutti i database supportano la clausola HAVING, le query di JDOQL che usano questa caratteristica sono del tutto portabili. Se manca il supporto del database, l'esclusione del risultato sarà realizzata mediante l'implementazione di JDO.

SELECT MAX(t0.BIDAMOUNT), t3.DESCRIPTION
FROM BID t0 INNER JOIN BIDDER t1 ON t0.BIDDER_JDOID = t1.JDOID INNER
JOIN REGISTEREDUSER t2 ON t1.USER_JDOID = t2.JDOID LEFT OUTER JOIN
AUCTIONITEM t3 ON t0.ITEM_JDOID = t3.JDOID
WHERE (t2.USERNAME = 'Sophie')
GROUP BY t0.ITEM_JDOID HAVING MAX(t0.BIDAMOUNT) > 10

 

 

Robin Roos è l'autore di of Java Data Objects (Addison-Wesley) e membro del gruppo di esperti di JDO 2.0 (JSR-243) e servizi professionali di VP presso SolarMetric.