11. Le langage HQL

Voici donc maintenant une présentation des aspects essentiels de HQL. Mon but n’étant pas de ré-écrire la documentation en ligne, et donc d’être exhaustif, le contenu qui suit se concentre sur les principes, avec des exemples illustratifs.

S1: HQL, aspects pratiques

Supports complémentaires :

Pour tester HQL, je vous recommande d’utiliser, sous Eclipse, le plugin Hibernate Tools. Voici comment l’installer, et quelques instructions d’utilisation.

  • Sous Eclipse, dans le menu Help, accédez au choix Marketplace (ou Software search and updates, selon les versions).
  • Lancez une recherche sur Hibernate Tools.
  • Lancez l’installation, puis relancez Eclipse quand elle est terminée.

Une fois le plugin installé, vous pouvez ouvrir des vues Eclipse. Dans le menu Windows, puis Show view, choisissez l’option Hibernate comme le montre la figure suivante:

_images/hibertools.png

Figure 1: Lancement des fenêtres de Hibernate Tools

Configuration

Lancez tout d’abord la fenêtre Hibernate Configurations. Vous pouvez créer des configurations (soit, essentiellement, le contenu d’un fichier hibernate.cfg.xml). Une configuration peut être créée à partir d’un projet existant, ce qui revient à utiliser les paramètres Hibernate déjà définis pour le projet.

Quand vous êtes sur l’onglet Configuration de la nouvelle fenêtre, des boutons sur la droite permettent de créer une nouvelle configuration; pour une configuration existante, le bouton droit donne accès à l’édition de la configuration, au lancement d’un éditeur HQL, etc.

_images/hibertools-config.png

Figure 2: Configuration d’une session Hibernate

Le plus simple est donc d’indiquer le projet associé à la configuration, et d’utiliser le fichier de mapping hibernate.cfg.xml de ce projet. Cela assure que les classes persistantes et la connexion à la base sont automatiquement détectées. L’outil vous permet également de créer ou modifier un fichier de configuration avec une interface graphique asez agréable.

Utilisation

Une fois la configuration créée, vous pouvez lancer un éditeur HQL et y saisir des requêtes. Pour l’éditeur HQL, sélectionnez votre configuration de session, et utilisez soit le bouton droit, soit l’icône « HQL » que vous voyez sur la barre d’outils. Vous pouvez saisir des requêtes HQL dans l’éditeur, et les exécuter avec la flèche verte.

Trois autres vues sont utiles:

  • Hibernate Dynamic SQL Preview vous montre la/les requête(s) SQL lancée(s) par Hibernate pour évaluer la requête HQL; très utile pour comprendre comment Hibernate matérialise le graphe requis par une requête HQL.
  • Hibernate Query Result vous donne simplement le résultat de la requête HQL.
  • Query parameters sert à définir la valeur des paramètres pour les requêtes qui en comportent.

Pour résumer, votre environnement de test HQL devrait ressembler à la figure ci-dessous.

_images/hibertools-views.png

Figure 3: Les fenêtres Hibernate Tools en action

Une dernière astuce: les requêtes HQL renvoient en général des objets, et Hibernate Tools affiche la référence de ces objets, ce qui n’est pas très parlant. Pour consulter plus facilement le résultat des requêtes, vous pouvez ajouter une clause select (optionnelle en HQL):

select film.titre, film.annee
from Film as film

HQL et java

L’intégration de HQL à Java se fait par des méthodes de la Session. Le code suivant recherche un (ou plusieurs) film(s) par leur titre.

public List<Film> parTitre(String titre)
{
        Query q = session.createQuery("from Film f where f.titre= :titre");
        q.setString ("titre", titre);
        return q.list();
}

La clause select est optionnelle en HQL, et offre peu d’intérêt dans le cadre de l’application java car on cherche en général à récupérer automatiquement des instances des classes mappées, ce qui implique de récupérer tous les attributs de chaque ligne.

Commme en jdbc, on peut introduire dans la requête des paramètres (ci-dessus, le titre) en les préfixant par « : » (« ? » est également accepté). Hibernate se charge de protéger la syntaxe de la requête, par exemple en ajoutant des barres obliques devant les apostrophes et autres caractères réservés. Voici une requête un peu plus complète, tout en étant formulée plus concisément.

session.createQuery("from Film as film where film.titre like :titre and film.annee < :annee")
               .setString ("titre", "%er%")
               .setInteger("annee", 2000)
               .list();

On applique la technique dite de « chaînage des méthodes », chaque méthode set renvoyant l’objet-cible, auquel on peut donc appliquer une nouvelle méthode, et ainsi de suite, pour une syntaxe beaucoup plus concise.

Notez également que l’affectation des paramètres tient compte de leur type: Hibernate propose des setDate(), setInteger(), setTimestamp(), etc. On peut également utiliser comme paramètre un objet persistant, comme le montre l’exemple suivant:

// bergman est une instance de Artiste
Artiste bergman = ...;

session.createQuery("from Film as film where film.realisateur= :mes")
               .setEntity ("mes", bergman)
               .list();

HQL offre la possibilité de paginer les résultats, une option souvent utile dans un contexte d’application web. L’exemple suivant retourne les lignes 10 à 19 du résultat.

session.createQuery("from Film")
               .setFirstResult (10)
               .setMaxResults(10)
               .list();

La méthode list() de l’objet query est la plus générale pour exécuter une requête et constituer le résultat. Si vous êtes sûr que ce dernier ne contient qu’un seul objet, vous pouvez utiliser uniqueResult().

session.createQuery("from Film where titre='Vertigo'")
                .uniqueResult();

Attention, une exception est levée si plus d’une ligne est trouvée. Cette méthode ne devrait être appliquée que pour des recherches portant sur des attributs déclarés unique dans le schéma de la base de données (dont la clé primaire).

Note

À partir de maintenant je donne les requêtes HQL indépendamment du code java, dans la forme « pure » que vous pouvez tester sous Hibernate Tools.

S2: base de HQL

Supports complémentaires :

Rappelons que HQL est un langage de requêtes objet qui sert à interroger le graphe (virtuel au départ) des objets java constituant la vue orientée-objet de la base. Autrement dit, on interroge un ensemble d’objets java liés par des associations, et pas directement la base relationnelle qui permet de les matérialiser. Hibernate se charge d’effectuer les requêtes SQL pour matérialiser la partie du graphe qui satisfait la requête. Cela donne une syntaxe et une interprétation parfois différente de SQL, même si les deux langages sont en apparence très proches.

Sélection d’objets

Toute cette section se concentre sur l’interrogation d’une seule classe.

La clause from

La forme la plus basique de HQL comprend la clause from, et indique la classe dont l’extension est sujet de la recherche:

from Film

Le point intéressant est que, en cas de hiérarchie d’héritage, il est possible d’interroger les super- et sous-classes. La liste des vidéos est obtenue par:

from Video

et la liste des films (sous-classe de Video) par:

from FilmV

On peut rechercher tous les objets persistants avec:

from java.lang.Object

On peut créer des alias pour désigner les objets et leur appliquer des traitements de sélection ou projection. La requête suivante définit un alias film:

select film.titre
from Film as film
where film.annee < 2000

Le mot-clé as est optionnel (comme la clause select). Je l’utilise systématiquement pour plus de clarté. Préfixer un attribut par l’alias n’est indispensable que s’il y a risque d’ambiguité, ce qui n’est pas le cas dans l’exemple ci-dessus.

La clause where

Les opérateurs de comparaison sont très proches de ceux de SQL: =, <, >, between, in, not between, etc. Les opérateurs like et not like s’utilisent comme en SQL et expriment la même chose. Les opérateurs logiques sont les mêmes qu’en SQL: and, or et not, avec ordre de priorité défini par le parenthésage. De même, HQL dispose d’une clause order by semblable à celle de SQL. Voici un exemple un peu complet:

from Film as film
where (titre like '%er%' and annee between 1970 and 2000)
or film.genre='Drame'
order by film.titre

Attention à la valeur NULL ou null. Comme en SQL, elle indique l’absence de valeur. On la teste avec l’expression is null, comme en SQL:

from Film as film
where  film.resume is null

Note

il existe beaucoup de fonctions pré-définies que peuvent être appliquées aux propriétés des objets: size(), minElement(), trunc(), trim(), etc. Je vous renvoie à la documentation pour une liste complète.

Jointures implicites

Contrairement à SQL où les critères de sélection ne peuvent concerner que les propriétés de la table interrogée, HQL permet la navigation vers des objets associés (et donc vers d’autres tables), avec une syntaxe abrégée et intuitive. On peut par exemple chercher les films dirigés par Clint Eastwood:

from Film as film
where film.realisateur.nom = 'Eastwood'

Cela ne fonctionne que pour les associations many-to-one et one-to-one. Vous ne pouvez pas écrire:

from Film as film
where film.roles.acteur.nom = 'Eastwood'

Attention, il n’y a pas de magie: cette syntaxe nécessite une jointure au niveau de la requête SQL générée par Hibernate, et correspond en fait à une manière dite implicite d’exprimer une jointure. Voici la requête SQL engendrée par Hibernate.

select *
from Film film0_ cross join Artiste artiste1_
where film0_.id_realisateur=artiste1_.id
and artiste1_.nom='Eastwood'

On peut bien entendu exprimer explicitement des jointures, avec des options assez riches détaillées dans la section suivante.

Exercice: expression de restrictions

  • Trouver lez films dont le titre comprend le mot “parrain”
  • Trouvez tous les drames
  • Trouvez tous les artistes nés avant 1970
  • Trouvez tous les réalisateurs (c-à-d. les artistes qui ont mis en scène un film)

S3: Les jointures

Supports complémentaires :

Jusqu’à présent, nous prenons les objets d’une classe, ce qui correspond donc à une recherche de ligne dans une seule table pour les instancier. Comme en SQL, il est nécessaire d’exprimer des jointures si l’on veut exprimer des critères de recherche sur les objets associés.

Jointures HQL et jointures SQL

Comme en SQL, une jointure s’exprime avec le join.

  • en HQL, en exprime des jointures par navigation en exploitant les associations d’objets définies dans le mapping,
  • en SQL, on applique (conceptuellement) le produit cartésien de deux tables, et on restreint le résultat à des combinaisons de lignes satisfaisant certains critères.

Il faut bien réaliser qu’une jointure par HQL qui, en apparence, accède directement aux objets associés, est en fait implantée par Hibernate comme une jointure SQL. Soyons concrets avec un premier exemple: on veut tous les films mis en scène par Clint Eastwood:

select film
from Film as film
join film.realisateur as a
where a.nom='Eastwood'

On accède donc directement à l’objet associé realisateur, sans mention explicite de la classe Artiste. En sous-main, Hibernate engendre et exécute la requête SQL suivante.

select film.*
from Film film0_  inner join  Artiste artiste1_
  on film0_.id_realisateur=artiste1_.id
where artiste1_.nom='Eastwood'

Vous voyez que la version SQL comprend une contrainte d’égalité entre la clé primaire de Artiste et la clé étrangère dans Film, contrainte qui n’apparaît pas en HQL puisqu’elle est représentée par une association entre objets dans le graphe virtuel. La définition du mapping permet à Hibernate d’engendrer le code SQL correct.

On peut adopter en HQL une expression plus proche de la logique SQL, mais moins naturelle du point de vue d’une logique « graphe d’objet ».

select film
from Film as film,  Artiste as a
where film.realisateur = a
and a.nom='Eastwood'

Le code SQL engendré est le même. Il n’y a donc aucune différence en termes de performance ou d’expressivité. Cette dernière version illustre une autre spécificité « objet » de HQL: on compare directement l’identité de deux objets (un artiste, un réalisateur) avec la clause:

where film.realisateur = a

et pas, comme en SQL, l’égalité des clés primaires de ces objets. Cela étant, il est possible d’effectuer directement une jointure sur les clés en HQL:

select film
from Film as film,  Artiste as a
where film.realisateur.id = a.id
and a.nom='Eastwood'

Ici, id est un mot-clé représentant la clé primaire, quel que soit son nom réel.

Le rôle de la clause select

Voici un autre exemple de jointure, impliquant une association un à plusieurs. Elle sélectionne tous les films dont un des rôles est McClane:

select distinct film
from Film as film
join film.roles as role
where role.nom= 'McClane'

La clause select prend ici une importance particulière: dans la mesure où nous accédons aux extensions de deux classes (Film et Role), il est préférable de préciser quels sont les objets que nous plaçons dans le résultat. Sans clause select, on obtient une liste de paires d’objets (un film, un rôle), soit une instance de List<Object[]>. Pour chaque élément de la liste, le premier composant du tableau est un Film, le second un Rôle.

De plus, le film est dupliqué autant de fois qu’il a de rôles associés, ce qui rend indispensable l’utilisation du mot-clé distinct. Executez par exemple la requête suivante.

select film.titre, role.nom
from Film as film
join film.roles as role

On obtient autant de fois le titre du film qu’il y a de rôles. Un tel résultat est assez laborieux à traiter, et on perd en grande partie les avantages de HQL (à savoir instancier un résultat directement exploitable).

Dernier exemple, tous les films où Clint Eastwood joue un rôle:

select distinct film
from Film as film
join film.roles as role
join role.pk.acteur as acteur
where acteur.nom= 'Eastwood'

Notez le chemin role.pk.acteur, exprimant un chemin dans le graphe des objets et composants. Je vous invite à consulter la requête SQL engendrée par Hibernate pour cette jointure.

La clause join de HQL

Les jointures de HQL sont les mêmes que celles du SQL ANSI.

  • [inner] join: exprime une jointure standard pour impliquer (pas forcément matérialiser) les objets associés;
  • left [outer] join: exprime une jointure externe à gauche: si par exemple un film n’a pas de metteur en scène, il (le film) fera quand même partie du résultat, avec un objet realisateur associé à null;
  • right [outer] join est la complémentaire de left join (et a donc peu d’intérêt en pratique).

Pour bien voir la différence, exécutez les deux requêtes suivantes:

from Film as film
inner join film.roles as role

et:

from Film as film
left outer join film.roles as role

La seconde devrait vous ramener aussi les films pour lesquels aucun rôle n’est connu.

Comme indiqué plus haut, cette requête renvoie une instance de List<Object[]>, qu’il faut ensuite décoder, pour obtenir, respectivement, des films et des rôles.

Ce qui soulève la question suivante: si, à partir de l’un des films sélectionnés, je veux accéder aux rôles par la collection film.roles, Hibernate a-t-il déjà instancié cette collection, ou va-t-il devoir à nouveau accéder à ces rôles avec une requête SQL?.

La réponse est: une nouvelle requête sera effectuée pour trouver les rôles du film, ce qui semble dommage car la jointure SQL sera effectuée deux fois. Les requêtes HQL que nous présentons de créent pas de graphe, mais instancient simplement les objets sélectionnés, sans lien entre eux. Dit autrement: le fait d’effectuer une jointure entre un film et des rôles n’implique pas que le graphe film-rôles est matérialisé dans le cache de premier niveau.

Ce n’est pas si illogique que cela en a l’air, si on prend en compte le fait qu’il est possible d’appliquer des restrictions sur les objets sélectionnés, comme dans l’exemple ci-dessous.

from Film as film
left outer join film.roles as role
where role.nom='McClane'

Si on matérialisait le graphe à partir des objets sélectionnés, la collection roles de chaque film serait incomplète puisqu’elle ne comprendrait que ceux dont le nom est McClane. En résumé, il faut bien distinguer deux motivations différentes, et partiellement incompatibles:

  • la sélection d’objets satisfaisant des critères de restriction;
  • la matérialisation d’une partie du sous-graphe de données.

Les requêtes HQL que nous présentons ici correspondent à la première motivation. Il est possible d’utiliser HQL aussi pour la matérialisation du graphe de données, avec une option fetch que nous étudierons dans le prochain chapitre.

Important

Retenez que ces deux motivations peuvent être incompatibles: si je restreins avec la clause where les objets sélectionnés (les films avec un rôle McClane), je ne peux pas obtenir en même temps un graphe complet (tous les rôles des films, dont un des rôles est McClane).

En résumé, concentrez-vous pour l’instant sur l’utilisation de HQL pour sélectionner et matérialiser les objets qui vous intéressent, sans chercher à optimiser les performances ou éliminer les redondances de requêtes SQL engendrées. Cela revient à toujours utiliser la clause select pour garder les objets qui vous intéressent, et à utiliser les jointures pour exprimer des restrictions. Par exemple:

select distinct film
from Film as film
left outer join film.roles as role
where role.nom='McClane'

Comme en SQL, une autre manière d’exprimer une jointure est d’utiliser des sous-requêtes, qui sont présentées un peu plus loin.

Exercice: pour quelques jointures de plus

  • Trouvez les films avec l’acteur Bruce Willis.
  • Affichez les noms des acteurs ayant joué le rôle de “Jack Dawson”, et le metteur en scène qui les a dirigés.
  • Affichez la liste des metteurs en scène, et les films qu’ils ont réalisés
  • Affichez les films dans lesquels le metteur en scène est également un acteur (pensez: « HQL, langage objet »!)
  • Affichez la liste des genres, et pour chaque genre les films correspondant.

S4: Compléments

Supports complémentaires :

Nous avons vu l’essentiel de HQL dans ce qui précède. Voici quelques compléments utiles.

Sous-requêtes

Comme en SQL, HQL permet l’expression de sous-requêtes, ce qui revient souvent à une nouvelle manière d’exprimer des jointures. Voici donc une nouvelle manière d’obtenir les films dirigés par Clint Eastwood.

from Film as film
where  film.realisateur in (from Artiste where nom='Eastwood')

Par rapport à l’expression standard avec un join, il existe une différence qui mérite d’être mentionnée: comme la classe Artiste n’apparaît que dans la sous-requête, les instances de Artiste ne figurent pas dans le résultat. Il n’est donc pas nécessaire d’utiliser une clause select pour se restreindre aux films.

Voici une autre version de la requête qui ramène les films dans lesquels figure un rôle “McClane”.

from Film as film
where  film in (select role.pk.film from Role as role
              where role.nom='McClane')

Et une autre version de la requête qui recherche les films dirigés par Clint Eastwood, cette fois avec une sous-requête exists.

select film.titre
from Film as film
where  exists (from Artiste as a where a = film.realisateur and a.nom='Eastwood')

Il est bien clair que cette expression est très compliquée par rapport à celle basée sur la jointure par navigation (et en particulier la jointure implicite, voir ci-dessus).

Les sous-requêtes en HQL sont tout à fait similaires à celles de SQL. Je n’entre donc pas dans l’énumération des diverses possibilités. Pour rappel, les quantificateurs suivants sont composables avec les opérateurs de comparaison, et s’appliquent à des sous-requêtes qui ramènent plusieurs lignes.

  • any: vrai si au moins une ligne ramenée par la sous-requête satisfait la comparaison;
  • all: vrai si toutes les lignes ramenées par la sous-requête satisfontt la comparaison.

Des synonymes pour any sont in et some. Voici deux exemples. Le premier recherche les films dont au moins un acteur est né avant 1940.

from Film as film
where 1940 > any (select  role.pk.acteur.anneeNaissance
                  from Role as role
                  where role.pk.film=film)

Il est beaucoup plus simple (et plus lisible) d’exprimer ce genre de requête avec un join. L’utilisation de all (pour: « les films dont tous les acteurs sont nés avant 1940 ») paraît plus justifiée:

from Film as film
where 1940 > all (select  role.pk.acteur.anneeNaissance
                  from Role as role
                  where role.pk.film=film)

Comme en SQL, il existe plusieurs syntaxes possibles pour une même requête. Par exemple, celle ci-dessus s’exprime aussi avec not exists (à vous de jouer).

Exercice: requêtes imbriquées

Les requêtes suivantes doivent être exprimées avec des sous-requêtes

  • Quels internautes n’ont donné que des notes supérieures à 3?
  • Quels sont les films dans lesquels joue Brad Pitt?
  • Et quels sont les films qui n’ont pas obtenu une note de 5?

Création d’objet spécifiques

Revenons sur les requêtes ad hoc qui constituent des résultats constitués de projection sur certains attributs. Voici un exemple

select film.titre, film.annee, film.realisateur.prenom, film.realisateur.nom
from Film as film

Quel est l’intérêt de cette requête? A priori elle implique un travail de décodage fastidieux du résultat, constitué d’une liste de tableaux, chaque tableau contenant 4 objets correspondant aux 4 attributs projetés.

Il existe quand même un avantage potentiel: comme la requête ne nécessite pas la création d’objets persistants, on évite le placement de ces objets dans le cache de la session, avec le (modeste) surcoût induit par le test d’existence, et l’insertion dans la table de hachage.

On gagne donc (un peu) en performance. Il se peut que dans certains cas le gain vaille le surcroît de programmation nécessaire.

Regroupement et agrégation

Le group by, le having et les fonctions d’agrégation sont identiques en HQL et en SQL (ou, pour être plus précis, elles ne présentent aucune spécificité supplémentaire par rapport à celles que nous avons déjà vues). Quelques exemples suffiront.

  • La note moyenne des films

    select avg(note)
    from Notation
    
  • Les films et leur nombre d’acteurs

    select film, count(*)
    from Film as film join film.roles as role
    group by film
    

    Notez qu’on aimerait faire select film, count(film.roles) from Film as film, mais HQL n’accepte pas cette syntaxe.

  • Les metteurs en scène ayant réalisé au moins trois films

    select artiste.nom, count(*)
    from Artiste as artiste join artiste.filmsRealises as film
    group by artiste
    having count(*)  > 3
    

Exercice: requêtes imbriquées

  • Les internautes et le nombre de films qu’ils ont notés
  • Donnez les films avec au moins 4 rôles.
  • Films dont la note moyenne est supérieure à la note moyenne de tous les films.