In JPA, per interrogare il database non si lavora direttamente con le tabelle, ma con le entità. Questo è reso possibile da JPQL (Java Persistence Query Language), un linguaggio dichiarativo simile a SQL, ma orientato agli oggetti. JPQL permette di esprimere query su classi, attributi e relazioni, astrando i dettagli del database e garantendo portabilità tra diversi sistemi relazionali.
L'approccio object-oriented di JPQL è uno dei pilastri fondamentali di JPA. Mentre SQL si concentra su tabelle e colonne, JPQL ragiona in termini di entità e proprietà, allineandosi perfettamente al modello di programmazione Java. Questo significa che le query riflettono la struttura del dominio applicativo piuttosto che lo schema del database, rendendo il codice più espressivo e meno dipendente dall'implementazione fisica della persistenza.
Un vantaggio cruciale di JPQL è la portabilità: le stesse query funzionano su MySQL, PostgreSQL, Oracle o qualsiasi altro database supportato dal provider JPA (come Hibernate o EclipseLink), che si occupa di tradurre JPQL nel dialetto SQL specifico. Questo elimina la necessità di riscrivere query quando si cambia database, riducendo drasticamente i costi di migrazione.
A differenza di SQL, in JPQL non si scrive SELECT * FROM users, ma si fa riferimento all'entità e ai suoi campi:
SELECT u FROM User u
Qui User è la classe dell'entità e u è un alias usato per riferirsi all'oggetto nella query. Questo approccio consente di navigare facilmente le relazioni tra entità tramite JOIN senza scrivere manualmente le chiavi esterne o tabelle di join.
È importante notare che JPQL distingue tra maiuscole e minuscole per i nomi delle entità e delle proprietà. Se la tua classe si chiama User, devi usare esattamente quel nome nella query, non user o USER. Al contrario, le parole chiave del linguaggio (SELECT, FROM, WHERE) sono case-insensitive, anche se per convenzione si scrivono in maiuscolo.
Sintassi base di JPQL
JPQL supporta le clausole fondamentali del linguaggio SQL, adattandole al contesto degli oggetti. La struttura generale di una query JPQL segue lo schema familiare di SQL, ma con alcune differenze semantiche importanti.
- SELECT: definisce quali entità o campi restituire. È possibile selezionare l'intera entità o solo specifici attributi. Esempio:
SELECT u.name, u.email FROM User u
Quando si selezionano singoli campi invece dell'entità completa, il risultato sarà un array di Object[] o, se si seleziona un solo campo, una lista di quel tipo specifico. Questa tecnica è utile per ottimizzare le prestazioni quando non servono tutti i dati dell'entità, riducendo il traffico tra database e applicazione.
- WHERE: filtra i risultati secondo condizioni sugli attributi. JPQL supporta gli operatori di confronto standard (
=,<>,<,>,<=,>=) e operatori logici (AND,OR,NOT).
SELECT u FROM User u WHERE u.name = 'Luca'
La clausola WHERE può utilizzare anche operatori avanzati come LIKE per pattern matching (WHERE u.name LIKE '%Luc%'), IN per verificare appartenenza a un insieme (WHERE u.status IN ('ACTIVE', 'PENDING')), BETWEEN per intervalli (WHERE u.age BETWEEN 18 AND 65), e IS NULL/IS NOT NULL per verificare valori nulli.
- ORDER BY: ordina i risultati in base a uno o più attributi. È possibile specificare l'ordinamento ascendente (
ASC, default) o discendente (DESC).
SELECT u FROM User u ORDER BY u.name ASC
Si possono combinare più criteri di ordinamento separandoli con virgole: ORDER BY u.lastName ASC, u.firstName ASC ordinerà prima per cognome e poi per nome in caso di cognomi uguali.
- JOIN: consente di navigare le relazioni tra entità, come in SQL ma a livello di oggetti. JPQL supporta diversi tipi di join:
INNER JOIN,LEFT JOIN, eLEFT OUTER JOIN.
SELECT o FROM Order o JOIN o.user u WHERE u.name = 'Luca'
Questa sintassi permette di esprimere query complesse senza conoscere la struttura fisica del database, rendendo il codice più leggibile e manutenibile. La notazione punto (o.user) sfrutta le relazioni già definite nelle entità tramite annotazioni come @ManyToOne o @OneToMany, eliminando la necessità di specificare manualmente le condizioni di join.
JPQL supporta anche fetch join, una caratteristica potente per risolvere il problema delle N+1 query. Scrivendo SELECT u FROM User u JOIN FETCH u.orders si caricano in un'unica query sia gli utenti che i loro ordini, evitando query aggiuntive quando si accede alla collezione. Questo è particolarmente importante per le prestazioni in presenza di lazy loading.
Funzioni aggregate e raggruppamento
JPQL offre le classiche funzioni aggregate di SQL per calcoli statistici:
COUNT: conta il numero di risultatiSUM: somma valori numericiAVG: calcola la mediaMAXeMIN: trovano il valore massimo e minimo
Esempio:
SELECT COUNT(u) FROM User u WHERE u.active = true
La clausola GROUP BY permette di raggruppare risultati, spesso combinata con HAVING per filtrare i gruppi:
SELECT u.department, COUNT(u)
FROM User u
GROUP BY u.department
HAVING COUNT(u) > 5
Questa query restituisce solo i dipartimenti con più di 5 utenti, dimostrando come JPQL supporti analisi complesse direttamente a livello di query.
Parametri nominati e posizionali
JPQL supporta due tipi di parametri per rendere le query dinamiche e sicure. I parametri prevengono SQL injection e migliorano la leggibilità del codice evitando concatenazioni di stringhe.
- Parametri nominati, indicati con
:nome, sono più leggibili e raccomandati per la loro chiarezza:
List<User> users = em.createQuery(
"SELECT u FROM User u WHERE u.name = :name", User.class)
.setParameter("name", "Luca")
.getResultList();
I parametri nominati sono preferibili perché rendono esplicito il significato di ogni valore e permettono di riordinare i parametri nella query senza doverli riassegnare nel codice Java. Inoltre, lo stesso parametro nominato può essere usato più volte nella stessa query.
- Parametri posizionali, indicati con
?1,?2, ecc., utilizzati meno frequentemente ma ancora supportati per compatibilità:
List<User> users = em.createQuery(
"SELECT u FROM User u WHERE u.name = ?1", User.class)
.setParameter(1, "Luca")
.getResultList();
I parametri consentono di scrivere query riutilizzabili senza concatenare stringhe, evitando errori di SQL injection. JPA si occupa automaticamente di fare l'escaping dei valori, garantendo la sicurezza anche quando i dati provengono da input utente non validato.
È possibile anche passare collezioni come parametri per query con l'operatore IN:
List<String> names = Arrays.asList("Luca", "Mario", "Anna");
List<User> users = em.createQuery(
"SELECT u FROM User u WHERE u.name IN :names", User.class)
.setParameter("names", names)
.getResultList();
Ottenere singoli risultati o liste
Le query JPQL possono restituire liste di entità o singoli risultati, a seconda della logica applicativa. La scelta del metodo appropriato è importante per evitare eccezioni runtime e scrivere codice robusto.
- Per ottenere una lista, anche vuota se non ci sono risultati:
List<User> users = em.createQuery(
"SELECT u FROM User u WHERE u.name = :name", User.class)
.setParameter("name", "Luca")
.getResultList();
Il metodo getResultList() restituisce sempre una lista, anche se vuota, quindi non genera mai eccezioni per mancanza di risultati. È il metodo più sicuro quando non si conosce a priori quanti record verranno restituiti.
- Per ottenere un singolo risultato, ad esempio quando si è certi che esiste esattamente un record:
User user = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email", User.class)
.setParameter("email", "luca@example.com")
.getSingleResult();
Il metodo getSingleResult() genera un'eccezione NoResultException se la query non restituisce alcun record, e NonUniqueResultException se restituisce più di un record. Va quindi usato con attenzione, solo quando si ha la certezza che esista esattamente un risultato. Per situazioni incerte, è preferibile usare getResultList() e verificare manualmente la dimensione della lista.
Paginazione dei risultati
Per gestire grandi volumi di dati, JPQL supporta la paginazione attraverso i metodi setFirstResult() e setMaxResults():
List<User> users = em.createQuery(
"SELECT u FROM User u ORDER BY u.name", User.class)
.setFirstResult(20)
.setMaxResults(10)
.getResultList();
Questo esempio recupera 10 utenti partendo dal ventunesimo record (offset 20), ideale per implementare paginazioni in interfacce web. JPA traduce questi metodi nelle clausole LIMIT e OFFSET appropriate per il database in uso.
Named Queries e riusabilità
Per query usate frequentemente, JPA permette di definire Named Queries direttamente sull'entità tramite l'annotazione @NamedQuery:
@Entity
@NamedQuery(
name = "User.findByName",
query = "SELECT u FROM User u WHERE u.name = :name"
)
public class User {
// ...
}
Le Named Queries vengono validate all'avvio dell'applicazione, individuando errori sintattici prima che il codice venga eseguito. Inoltre, possono essere ottimizzate e cachate dal provider JPA per migliori prestazioni.
Conclusione
JPQL rappresenta il ponte tra il mondo relazionale dei database e il mondo orientato agli oggetti di Java. Permette di scrivere query potenti, leggibili e indipendenti dal database fisico, lavorando direttamente con le entità e le loro relazioni. Comprendere la sintassi di base, l'uso dei parametri e la differenza tra liste e singoli risultati è fondamentale per sfruttare appieno la persistenza JPA. Quando le query scorrono fluide e i dati tornano esattamente come previsto, il lavoro con JPA diventa naturale, intuitivo e… daje.