Requêtes : les langages JPQL et HQL
Introduction
Java Persistence Query Language (JPQL) et Hibernate Query Language (HQL) sont deux langages de requêtes focalisés sur la manipulation d’objets et similaires au langage SQL.
Du fait qu’Hibernate était une référence lors de l’établissement de la norme JPA, JPQL est fortement inspiré par HQL.
Le principe de ces langages est de manipuler les propriétés des objets dans les requêtes et ainsi ne pas dépendre des noms de tables/colonnes.
À savoir : une requête JPQL est toujours une requête HQL valide, cependant l’inverse n’est pas vrai.
Généralités
Une requête JPQL, comme une requête HQL ou encore SQL, peut être séparée en cinq parties : le type d’opération, le périmère de la requête, les restrictions, le regroupement et le tri.
-
Le type d’opération correspond au type de requête : INSERT, SELECT, UPDATE, DELETE.
-
Le périmètre de la requête correspond à la clause FROM et les jointures.
-
Les restrictions correspondent à la clause WHERE.
-
Le regroupement correspond aux clauses GROUP BY et HAVING.
-
Le tri correspond à la clause ORDER BY.
De ce fait, il n’y a pas de différence fonctionnelle entre une requête orientée objet et une requête classique SQL.
Types de requêtes
Il est possible de faire en JPQL et HQL des SELECT, UPDATE et DELETE. HQL permet en plus de faire des INSERT.
Lors de modifications importantes de la base de données, il est fortement recommandé de faire attention car il est possible que le cache ne soit plus synchronisé avec la base de données.
La prudence devrait être de mise lors de l’exécution d’opérations de mise à jour majeure ou de suppression, car elles peuvent entraîner des incohérences entre la base de données et les entités dans le contexte de persistance actif. En général, les mises à jour majeures et les opérations de suppression doivent être effectuées dans une transaction dans un nouveau contexte de persistance ou avant de lire ou d’accéder à des entités dont l’état pourrait être touché par de telles opérations. Spécification JPA 2.1 - 4.10.
1. SELECT
Une requête SELECT se compose de :
-
Clause SELECT
-
Clause FROM
-
Clause WHERE
-
Clause GROUP BY
-
Clause HAVING
-
Clause ORDER BY
Les clauses SELECT et FROM sont obligatoires en JPQL. Seule la clause FROM est obligatoire en HQL, mais par convention, il est recommandé d’écrire également le SELECT.
Une requête de sélection est exécutée depuis javax.persistence.Query...
La clause SELECT
La clause SELECT sert à définir le type de retour de l’exécution de la requête. Comme en SQL, il est possible de retourner un tuple en entier ou une partie uniquement. La différence avec le SQL c’est que l’opération s’effectue sur une entité et non un tuple.
Pour sélectionner toutes les personnes :
SELECT p FROM Personne p
Retourne List<Personne>.
Pour sélectionner tous les noms :
SELECT p.nom FROM Personne p
Retourne List<String>.
Pour sélectionner tous les noms et prénoms :
SELECT p.nom , p.prenom FROM Personne p
Retourne List<Object[]>.
Il est possible de retourner différentes expressions (voir la section Les expressions de ce chapitre).
Il existe certaines expressions, uniquement disponibles pour la clause SELECT, appelées constructor expression en JPQL et dynamic instantiation en HQL.
SELECT new PersonneSimple(p.nom, p.prenom)
FROM Personne
Retourne List<PersonneSimple>.
Au lieu d’avoir un tableau d’objets, les valeurs sont encapsulées dans un objet défini. La classe référence doit être entièrement qualifiée et son constructeur doit être identique.
public class PersonneSimple {
private String nom;
private String prenom;
public PersonneSimple(String nom...
La clause FROM
La clause FROM est responsable du périmètre de la requête, c’est-à-dire des entités auxquelles les autres clauses auront accès.
Elle est aussi responsable des variables d’identification pour la requête.
1. Variables d’identification
Les variables d’identification sont plus communément appelées « alias ». Ce sont des références aux objets afin de pouvoir les manipuler plus facilement.
Selon JPQL, les variables d’identification ne sont pas sensibles à la casse, c’est-à-dire qu’un ORM devrait pouvoir interpréter toute variable peu importe sa casse.
Afin de faciliter la lecture, il est fortement recommandé de garder la même écriture pour tous les alias.
2. Référence à l’entité root
La référence à l’entité root correspond à une référence à une entité mappée depuis l’application.
Pour faire référence à une entité, il suffit de d’écrire le nom de l’entité complet ou non.
Par exemple, pour faire référence à l’entité com.eni.jpa.entity.Personne, il y a trois possibilités :
-
En utilisant la dénomination la plus complète :
SELECT p FROM com.eni.jpa.entity.Personne AS p
-
Sans...
La clause WHERE
La clause WHERE permet de filtrer les données impactées par le type d’opération (SELECT, UPDATE, DELETE). Des quatre clauses facultatives, elle est la plus utilisée.
Les alias utilisés dans la clause WHERE doivent forcément être définis dans le périmètre de la requête.
La clause WHERE s’opère sur des booléens qui sont le résultat de différentes opérations (prédicats).
SELECT e
FROM Entite AS e
WHERE condition
Comme en SQL, il est possible de restreindre sur plusieurs conditions avec les opérations sur les booléens (AND, OR, ()).
Par exemple, la requête suivante permet de sélectionner tous les pays dont le nom commence par ’F’ et dont la monnaie est l’euro, ou ceux dont la langue est le français :
SELECT p
FROM Pays p
WHERE (p.nom like 'F%' AND p.monnaie='Euro')
OR p.langueList.nom = 'Français'
Les expressions
Une expression représente une valeur, que ce soit une donnée littérale (booléen, String…), une variable (alias, PATH, paramètre) ou le résultat d’une fonction.
1. Alias
Les alias correspondent aux variables d’identification expliquées à la section La clause FROM - Variables d’identification.
2. PATH
Les expressions de PATH sont expliquées à la section La clause FROM - Les jointures.
3. Paramètres
Afin de pouvoir modifier une requête après sa construction, il est possible de mettre en place des paramètres, JPQL en supporte de deux types :
-
Les paramètres nommés.
-
Les paramètres de position.
Les paramètres nommés s’écrivent de la façon suivante :
:nomDuParamètre
Ils peuvent être présents plusieurs fois dans la requête pour être réutilisés, ce qui donne :
String requete = "SELECT p FROM Pays p "+
"WHERE p.nom = :nom "+
"OR p.langueList.nom = :nom";
String leNomAChercher = ...
List<Pays> resultat = em.createQuery(requete,Pays.class).
setParameter("nom",leNomAChercher).getResultList();
Les paramètres de position s’écrivent de la façon suivante :
?numéro
Ils peuvent être présents plusieurs fois dans la requête pour être réutilisés, ce qui donne :
String requete = "SELECT p FROM Pays p "+
"WHERE p.nom = ?1 "+
"OR p.langueList.nom = ?1";
String leNomAChercher = ...
List<Pays> resultat =
em.createQuery(requete,Pays.class).setParameter(1,
leNomAChercher).getResultList();
4. Littéral
Les expressions littérales représentent une constante pour la requête. JPQL supporte différents types de littéraux tels que le NULL, les booléens, les numériques, les strings, les dates, les énum et les types d’entités.
a. Le littéral NULL
Le littéral NULL représente une valeur nulle, et comme JPQL n’est pas sensible à la casse, il peut s’écrire NULL, null, nUll… Il est de bonne pratique de garder la même casse tout au long du projet.
Afin de tester...
Les clauses GROUP BY et HAVING
La clause GROUP BY permet de regrouper les résultats. Le regroupement ne permet plus de sélectionner une entité et uniquement certaines valeurs sont accessibles :
-
Les propriétés du regroupement.
-
Les fonctions d’agrégations (count, sum, avg, min, max).
Le GROUP BY s’effectue, lors de l’exécution de la requête, entre la clause WHERE et la clause SELECT.
Par exemple, la requête suivante permet de sélectionner la langue parlée par les pays du continent Europe ainsi que le nombre de pays concernés.
SELECT l.nom,count(p)
FROM Pays p JOIN p.langueList l
WHERE p.continent = 'Europe'
GROUP BY l.nom
Il est possible de filtrer les informations après le GROUP BY via la clause HAVING ; uniquement les valeurs du GROUP BY ou les fonctions d’agrégations sont autorisées.
Par exemple, la requête suivante permet d’additionner par langue, la population des pays du continent Europe parlant cette langue, puis de retourner uniquement les langues étant parlées au minimum par deux pays.
SELECT l.nom, sum(p.population)
FROM Pays p JOIN p.langueList l
WHERE p.continent = 'Europe'
GROUP BY l.nom
HAVING count(p) > 1
La clause ORDER BY
La clause ORDER BY permet de trier les résultats. Toute requête JPQL n’ayant pas de clause ORDER BY produit un résultat non trié.
Lors de l’exécution de la requête, l’ORDER BY s’effectue en dernier, après le SELECT.
Par exemple, la requête suivante sélectionne le nom des pays du continent Europe triés par nom.
SELECT p.nom
FROM Pays p
WHERE p.continent = 'Europe'
ORDER BY p.nom
L’ordre de tri peut être soit ascendant (ASC), soit descendant (DESC). Par défaut, le tri est ascendant.
// tri par défaut (ascendant)
SELECT p.nom FROM Pays p ORDER BY p.nom
//équivalent
SELECT p.nom FROM Pays p ORDER BY p.nom ASC
// tri descendant
SELECT p.nom FROM Pays p ORDER BY p.nom DESC
Il est possible de trier sur différents critères en les ajoutant les uns à la suite des autres, séparés par une virgule. Chaque critère de tri additionnel sera pris en compte uniquement si l’ordre de tri précédent est identique.
La requête suivante trie les pays par monnaie en ordre décroissant, puis lorsque plusieurs pays ont la même monnaie, le tri s’effectue sur le nom du pays par ordre croissant :
SELECT p.nom, p.monnaie FROM Pays p
ORDER BY p.monnaie DESC, p.nom
Les valeurs prises...
L’UPDATE
Comme vu dans le chapitre Manipulation des données - Modification d’une entité, JPA permet la mise à jour des données via la manipulation d’objets dans le code source du projet. Il est également possible de créer des requêtes JPQL afin de modifier ces données.
Mettre à jour les données directement via une requête JPQL est au moins aussi rapide que récupérer l’objet, le mettre à jour puis le sauvegarder. Par contre, il faut être prudent car mettre à jour des valeurs sans passer par l’EntityManager risque de le désynchroniser avec la base de données (il est possible que l’EntityManager ne soit pas conscient qu’une entité dans son cache a été mise à jour). Une bonne pratique est d’utiliser un autre EntityManager afin de faire les requêtes de mise à jour.
Les opérations de mise à jour ne peuvent être exécutées que lors d’une transaction active. Les changements sont visibles, depuis un autre EntityManager, uniquement lors du commit.
1. Mise à jour de toutes les données
L’UPDATE sans restriction met à jour toutes les données.
Les trois requêtes suivantes, qui sont équivalentes, permettent de mettre le nom des pays en majuscules.
UPDATE Pays SET nom = UPPER(nom)
UPDATE Pays...
Le DELETE
Comme vu dans le chapitre Manipulation des données - Suppression d’une entité, JPA permet la suppression des données via la manipulation d’objets dans le code source du projet. Il est également possible de créer des requêtes JPQL afin de supprimer ces données.
Supprimer les données directement via une requête JPQL est au moins aussi rapide que de récupérer l’objet puis le supprimer. Par contre, il faut être prudent car effacer des données sans passer par l’EntityManager risque de le désynchroniser avec la base de données (il est possible que l’EntityManager ne soit pas conscient qu’une entité dans son cache a été supprimée). Une bonne pratique est d’utiliser un autre EntityManager afin de faire les requêtes DELETE.
Les opérations de suppression ne peuvent être exécutées que lors d’une transaction active. Les changements sont visibles, depuis un autre EntityManager, uniquement lors du commit.
1. Supprimer toutes les données
Le DELETE sans restriction permet de supprimer toutes les données.
Il se compose des mots-clés DELETE FROM suivis du nom de l’entité à supprimer.
Par exemple, les trois requêtes suivantes, qui sont équivalentes, permettent de supprimer les données de l’entité...
L’INSERT
La requête INSERT n’est pas prise en compte par JPQL, mais elle est possible en HQL.
Contrairement à une requête d’insertion SQL, où il est possible d’insérer des données arbitraires, l’INSERT de HQL ne peut être utilisé que pour insérer des données construites depuis une requête de sélection. Son application est donc limitée.
La requête suivante permet de copier les pays de la table Pays vers la table SavePays :
INSERT INTO SavePays(id,nom)
SELECT id, nom FROM Pays