IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]


 

56. Hibernate

 

chapitre 5 6

 

Niveau : niveau 4 Supérieur 

 

Hibernate est une solution open source de type ORM (Object Relational Mapping) qui permet de faciliter le développement de la couche persistance d'une application. Hibernate permet donc de représenter une base de données en objets Java et vice versa.

Hibernate facilite la persistence et la recherche de données dans une base de données en réalisant lui-même la création des objets et les traitements de remplissage de ceux-ci en accédant à la base de données. La quantité de code ainsi épargnée est très importante d'autant que ce code est généralement fastidieux et redondant.

Hibernate est très populaire notamment à cause de ses bonnes performances et de son ouverture à de nombreuses bases de données.

Les bases de données supportées sont les principales du marché : DB2, Oracle, MySQL, PostgreSQL, Sybase, SQL Server, Sap DB, Interbase, ...

Le site officiel http://www.hibernate.org contient beaucoup d'informations sur l'outil et propose de le télécharger ainsi que sa documentation.

La version utilisée dans cette section est la 2.1.2 : il faut donc télécharger le fichier hibernate-2.1.2.zip et le décompresser dans un répertoire du système.

Ce chapitre va utiliser Hibernate avec une base de données de type MySQL possédant une table nommée "personnes".

Hibernate a besoin de plusieurs éléments pour fonctionner :

  • une classe de type javabean qui encapsule les données d'une occurrence d'une table
  • un fichier de configuration qui assure la correspondance entre la classe et la table (mapping)
  • des propriétés de configuration notamment des informations concernant la connexion à la base de données

Une fois ces éléments correctement définis, il est possible d'utiliser Hibernate dans le code des traitements à réaliser. L'architecture d'Hibernate est donc la suivante :

Ce chapitre survole uniquement les principales fonctionnalités d'Hibernate qui est un outil vraiment complet : pour de plus amples informations, il est nécessaire de consulter la documentation officielle fournie avec l'outil ou consultable sur le site web.

Ce chapitre contient plusieurs sections :

 

56.1. La création d'une classe qui va encapsuler les données

Cette classe doit respecter le standard des Javabeans, notamment, encapsuler les propriétés dans ses champs private avec des getters et setters et avoir un constructeur par défaut.

Les types utilisables pour les propriétés sont : les types primitifs, les classes String et Dates, les wrappers, et n'importe quelle classe qui encapsule une autre table ou une partie de la table.

Exemple :
import java.util.Date;

public class Personnes {

  private Integer idPersonne;
  private String nomPersonne;
  private String prenomPersonne;
  private Date datenaissPersonne;

  public Personnes(String nomPersonne, String prenomPersonne, Date datenaissPersonne) {
    this.nomPersonne = nomPersonne;
    this.prenomPersonne = prenomPersonne;
    this.datenaissPersonne = datenaissPersonne;
  }

  public Personnes() {
  }

  public Date getDatenaissPersonne() {
    return datenaissPersonne;
  }

  public Integer getIdPersonne() {
    return idPersonne;
  }

  public String getNomPersonne() {
    return nomPersonne;
  }

  public String getPrenomPersonne() {
    return prenomPersonne;
  }

  public void setDatenaissPersonne(Date date) {
    datenaissPersonne = date;
  }

  public void setIdPersonne(Integer integer) {
    idPersonne = integer;
  }

  public void setNomPersonne(String string) {
    nomPersonne = string;
  }

  public void setPrenomPersonne(String string) {
    prenomPersonne = string;
  }
}

 

56.2. La création d'un fichier de correspondance

Pour assurer le mapping, Hibernate a besoin d'un fichier de correspondance (mapping file) au format XML qui va contenir des informations sur la correspondance entre la classe définie et la table de la base de données.

Même si cela est possible, il n'est pas recommandé de définir un fichier de mapping pour plusieurs classes. Le plus simple est de définir un fichier de mapping par classe, nommé du nom de la classe suivi par ".hbm.xml". Ce fichier doit être situé dans le même répertoire que la classe correspondante ou dans la même archive pour les applications packagées.

Différents éléments sont précisés dans ce document XML :

  • la classe qui va encapsuler les données
  • l'identifiant dans la base de données et son mode de génération
  • le mapping entre les propriétés de classe et les champs de la base de données
  • les relations
  • ...

Le fichier débute par un prologue et une définition de la DTD utilisée par le fichier XML.

Exemple :
<?xml version="1.0"?>
  <!DOCTYPE hibernate-mapping
    PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

Le tag racine du document XML est le tag <hibernate-mapping>. Ce tag peut contenir un ou plusieurs tag <class> : il est cependant préférable de n'utiliser qu'un seul tag <class> et de définir autant de fichiers de correspondance que de classes.

Exemple :

Exemple :
<?xml version="1.0"?><!DOCTYPE hibernate-mapping
  PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"><hibernate-mapping>
  <class name="Personnes" table="personnes">
    <id name="idPersonne" type="int" column="idpersonne">
      <generator class="native"/>
    </id>
    <property name="nomPersonne" type="string" not-null="true" />
    <property name="prenomPersonne" type="string" not-null="true" />
    <property name="datenaissPersonne" type="date">
      <meta attribute="field-description">date de naissance</meta>
    </property>
  </class>
</hibernate-mapping>

Le tag <class> permet de préciser des informations sur la classe qui va encapsuler les données.

Ce tag possède plusieurs attributs dont les principaux sont:

Nom

Obligatoire

Rôle

name

oui

nom pleinement qualifié de la classe

table

oui

nom de la table dans la base de données

dynamic-update

non

booléen qui indique de ne mettre à jour que les champs dont la valeur a été modifiée (false par défaut)

dynamic-insert

non

booléen qui indique de ne générer un ordre insert que pour les champs dont la valeur est non nulle (false par défaut)

mutable

non

booléen qui indique si les occurrences peuvent être mises à jour (true par défaut)


Le tag enfant <id> du tag <class> permet de fournir des informations sur l'identifiant d'une occurrence dans la table.

Ce tag possède plusieurs attributs :

Nom

Obligatoire

Rôle

name

non

nom de la propriété dans la classe

type

non

le type Hibernate

column

non

le nom du champ dans la base de données (par défaut le nom de la propriété)

unsaved-value

non

permet de préciser la valeur de l'identifiant pour une instance non encore enregistrée dans la base de données. Les valeurs possibles sont : any, none, null ou une valeur fournie. Null est la valeur par défaut.


Le tag <generator>, fils obligatoire du tag <id>, permet de préciser quel est le mode de génération d'un nouvel identifiant.

Ce tag possède un attribut :

Attribut

Obligatoire

Rôle

class

oui

précise la classe qui va assurer la génération de la valeur d'un nouvel identifiant. Il existe plusieurs classes fournies en standard par Hibernate qui possèdent un nom utilisable comme valeur de cet attribut.


Les classes de génération fournies en standard par Hibernate possèdent chacun un nom :

Nom

Rôle

increment

incrémentation d'une valeur dans la JVM

identity

utilisation d'un identifiant auto-incrémenté pour les bases de données qui le supportent (DB2, MySQL, SQL Server, ...)

sequence

utilisation d'une séquence pour les bases de données qui le supportent (Oracle, DB2, PostgreSQL, ...)

hilo

utilisation d'un algorithme qui utilise une valeur réservée pour une table d'une base de données (par exemple une table qui stocke la valeur du prochain identifiant pour chaque table)

seqhilo

idem mais avec un mécanisme proche d'une séquence

uuid.hex

utilisation d'un algorithme générant un identifiant de type UUID sur 32 caractères prenant en compte entre autres l'adresse IP de la machine et l'heure du système

uuid.string

idem générant un identifiant de type UUID sur 16 caractères

native

utilise la meilleure solution proposée par la base de données

assigned

la valeur est fournie par l'application

foreign

la valeur est fournie par un autre objet avec lequel la classe est associée


Certains modes de génération nécessitent des paramètres : dans ce cas, il faut les définir en utilisant un tag fils <param> pour chaque paramètre.

Le tag <property>, fils du tag <class>, permet de fournir des informations sur une propriété et sa correspondance avec un champ dans la base de données.

Ce tag possède plusieurs attributs dont les principaux sont :

Nom

Obligatoire

Rôle

name

oui

précise le nom de la propriété

type

non

précise le type

column

non

précise le nom du champ dans la base de données (par défaut le nom de la propriété)

update

non

précise si le champ est mis à jour lors d'une opération SQL de type update (par défaut true)

insert

non

précise si le champ est mis à jour lors d'une opération SQL de type insert (par défaut true)


Le type doit être soit un type Hibernate (integer, string, date, timestamp, ...), soit les types primitif Java ou de certaines classes de base (int, java.lang.String, float, java.util.Date, ...), soit une classe qui encapsule des données à rendre persistantes.

Le fichier de correspondance peut aussi contenir une description des relations qui existent avec la table dans la base de données.

 

56.3. Les propriétés de configuration

Pour exécuter Hibernate, il faut lui fournir un certain nombre de propriétés concernant sa configuration pour qu'il puisse se connecter à la base de données.

Ces propriétés peuvent être fournies sous plusieurs formes :

  • un fichier de configuration nommé hibernate.properties et stocké dans un répertoire inclus dans le classpath
  • un fichier de configuration au format XML nommé hibernate.cfg.xml
  • utiliser la méthode setProperties() de la classe Configuration
  • définir des propriétés dans la JVM en utilisant l'option -Dpropriété=valeur

Les principales propriétés pour configurer la connexion JDBC sont :

Nom de la propriété

Rôle

hibernate.connection.driver_class

nom pleinement qualifié de la classe du pilote JDBC

hibernate.connection.url

URL JDBC désignant la base de données

hibernate.connection.username

nom de l'utilisateur pour la connexion

hibernate.connection.password

mot de passe de l'utilisateur

hibernate.connection.pool_size

nombre maximum de connexions dans le pool


Les principales propriétés à utiliser pour configurer une source de données (DataSource) sont :

Nom de la propriété

Rôle

hibernate.connection.datasource

nom du DataSource enregistré dans JNDI

hibernate.jndi.url

URL du fournisseur JNDI

hibernate.jndi.class

classe pleinement qualifiée de type InitialContextFactory permettant l'accès à JNDI

hibernate.connection.username

nom de l'utilisateur de la base de données

hibernate.connection.password

mot de passe de l'utilisateur


Les principales autres propriétés sont :

Nom de la propriété

Rôle

hibernate.dialect

nom de la classe pleinement qualifié qui assure le dialogue avec la base de données

hibernate.jdbc.use_scrollable_resultset

booléen qui permet le parcours dans les deux sens pour les connexions fournies à Hibernate utilisant des pilotes JDBC 2 supportant cette fonctionnalité

hibernate.show_sql

booléen qui précise si les requêtes SQL générées par Hibernate sont affichées dans la console (particulièrement utile lors du débogage)


Hibernate propose des classes qui héritent de la classe Dialect pour chaque base de données supportée. C'est le nom de la classe correspondant à la base de données utilisée qui doit être obligatoirement fourni à la propriété hibernate.dialect.

Pour définir les propriétés utiles, le plus simple est de définir un fichier de configuration qui en standard doit se nommer hibernate.properties. Ce fichier contient une paire clé=valeur pour chaque propriété définie.

Exemple : paramètres pour utiliser une base de données MySQL
hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class=com.mysql.jdbc.Driver
hibernate.connection.url=jdbc:mysql://localhost/testDB
hibernate.connection.username=root
hibernate.connection.password=

Le pilote de la base de données utilisée, mysql-connector-java-3.0.11-stable-bin.jar dans l'exemple, doit être ajouté dans le classpath.

Il est aussi possible de définir les propriétés dans un fichier au format XML nommé en standard hibernate.cfg.xml

Les propriétés sont alors définies par un tag <property>. Le nom de la propriété est définie grâce à l'attribut « name » et sa valeur est fournie dans le corps du tag.

Exemple :
<?xml version='1.0' encoding='utf-8'?> 
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 2.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost/testDB</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password"></property>
    <property name="dialect">net.sf.hibernate.dialect.MySQLDialect</property>
    <property name="show_sql">true</property>
    <mapping resource="Personnes.hbm.xml"/>
  </session-factory>
</hibernate-configuration>

 

56.4. L'utilisation d'Hibernate

Pour utiliser Hibernate dans le code, il est nécessaire de réaliser plusieurs opérations :

  • création d'une instance de la classe
  • création d'une instance de la classe SessionFactory
  • création d'une instance de la classe Session qui va permettre d'utiliser les services d'Hibernate

Si les propriétés sont définies dans le fichier hibernate.properties, il faut tout d'abord créer une instance de la classe Configuration. Pour lui associer la ou les classes encapsulant les données, la classe propose deux méthodes :

  • addFile() qui attend en paramètre le nom du fichier de mapping
  • addClass() qui attend en paramètre un objet de type Class encapsulant la classe. Dans ce cas, la méthode va rechercher un fichier nommé nom_de_la_classe.hbm.xml dans le classpath (ce fichier doit se situer dans le même répertoire que le fichier .class de la classe correspondante)

Une instance de la classe Session est obtenue à partir d'une fabrique de type SessionFactory, elle-même obtenue à partir de l'instance du type Configuration en utilisant la méthode buildSessionFactory().

La méthode openSession() de la classe SessionFactory permet d'obtenir une instance de la classe Session.

Par défaut, c'est la méthode openSession() qui va ouvrir une connexion vers la base de données en utilisant les informations fournies par les propriétés de configuration.

Exemple :
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
import java.util.Date;

public class TestHibernate1 {
  
  public static void main(String args[]) throws Exception {
    Configuration config = new Configuration();
    config.addClass(Personnes.class);
    SessionFactory sessionFactory = config.buildSessionFactory();
    Session session = sessionFactory.openSession();
...

Il est aussi possible de fournir en paramètre de la méthode openSession() une instance de la classe javax.sql.Connection qui encapsule la connexion à la base de données.

Pour une utilisation du fichier hibernate.cfg.xml, il faut créer une occurrence de la classe Configuration, appeler sa méthode configure() qui va lire le fichier XML et appeler la méthode buildSessionFactory() de l'objet renvoyé par la méthode configure().

Exemple :
import net.sf.hibernate.*; 
import net.sf.hibernate.cfg.Configuration; 
import java.util.*; 

public class TestHibernate1 { 
  public static void main(String args[]) throws Exception { 
    SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); 
...

Il est important de clôturer l'objet Session, une fois que celui-ci est devenu inutile, en utilisant la méthode close().

 

56.5. La persistance d'une nouvelle occurrence

Pour créer une nouvelle occurrence dans la source de données, il suffit de créer une nouvelle instance de la classe encapsulant les données, de valoriser ses propriétés et d'appeler la méthode save() de la session en lui passant en paramètre l'objet encapsulant les données.

La méthode save() n'a aucune action directe sur la base de données. Pour enregistrer les données dans la base, il faut réaliser un commit sur la connexion ou la transaction ou faire appel à la méthode flush() de la classe Session.

Exemple :
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
import java.util.Date;

public class TestHibernate1 {
  
  public static void main(String args[]) throws Exception {
    Configuration config = new Configuration();
    config.addClass(Personnes.class);
    SessionFactory sessionFactory = config.buildSessionFactory();
    Session session = sessionFactory.openSession(); 

    Transaction tx = null; 
    try { 
      tx = session.beginTransaction(); 
      Personnes personne = new Personnes("nom3", "prenom3", new Date());
      session.save(personne);
      session.flush() ;
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally { 
      session.close(); 
    } 
    
    sessionFactory.close(); 
  } 
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate1
Buildfile: build.xml
init:
[copy] Copying 1 file to C:\java\test\testhibernate\bin
compile:
TestHibernate1:
[java] 12:41:37,402 INFO Environment:462 - Hibernate 2.1.2
[java] 12:41:37,422 INFO Environment:496 - loaded properties from resource
hibernate.properties: {hibernate.connection.username=root, hibernate.connection
.password=, hibernate.cglib.use_reflection_optimizer=true, hibernate.dialect=net
.sf.hibernate.dialect.MySQLDialect, hibernate.connection.url=jdbc:mysql://localh
ost/testDB, hibernate.connection.driver_class=com.mysql.jdbc.Driver}
[java] 12:41:37,432 INFO Environment:519 - using CGLIB reflection optimizer
[java] 12:41:37,502 INFO Configuration:329 - Mapping resource: Personnes.hbm.xml
[java] 12:41:38,784 INFO Binder:229 - Mapping class: Personnes -> personnes
[java] 12:41:38,984 INFO Configuration:595 - processing one-to-many association mappings
[java] 12:41:38,994 INFO Configuration:604 - processing one-to-one association property 
references
[java] 12:41:38,994 INFO Configuration:629 - processing foreign key constraints
[java] 12:41:39,074 INFO Dialect:82 - Using dialect: net.sf.hibernate.dialect.MySQLDialect
[java] 12:41:39,084 INFO SettingsFactory:62 - Use outer join fetching: true
[java] 12:41:39,104 INFO DriverManagerConnectionProvider:41 - Using Hibernate 
built-in connection pool (not for production use!)
[java] 12:41:39,114 INFO DriverManagerConnectionProvider:42 - Hibernate co
nnection pool size: 20
[java] 12:41:39,144 INFO DriverManagerConnectionProvider:71 - using driver
: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost/testDB
[java] 12:41:39,154 INFO DriverManagerConnectionProvider:72 - connection p
roperties: {user=root, password=}
[java] 12:41:39,185 INFO TransactionManagerLookupFactory:33 - No Transacti
onManagerLookup configured (in JTA environment, use of process level read-write
cache is not recommended)
[java] 12:41:39,625 INFO SettingsFactory:102 - Use scrollable result sets:true
[java] 12:41:39,635 INFO SettingsFactory:105 - Use JDBC3 getGeneratedKeys(): true
[java] 12:41:39,635 INFO SettingsFactory:108 - Optimize cache for minimal puts: false
[java] 12:41:39,635 INFO SettingsFactory:117 - Query language substitutions: {}
[java] 12:41:39,645 INFO SettingsFactory:128 - cache provider: net.sf.ehcache.hibernate.
Provider
[java] 12:41:39,685 INFO Configuration:1080 - instantiating and configuring caches
[java] 12:41:39,946 INFO SessionFactoryImpl:119 - building session factory
[java] 12:41:41,237 INFO SessionFactoryObjectFactory:82 - no JNDI name configured
[java] 12:41:41,768 INFO SessionFactoryImpl:531 - closing
[java] 12:41:41,768 INFO DriverManagerConnectionProvider:137 - cleaning up
connection pool: jdbc:mysql://localhost/testDB
BUILD SUCCESSFUL
Total time: 7 seconds
C:\java\test\testhibernate>

 

56.6. L'obtention d'une occurrence à partir de son identifiant

La méthode load() de la classe Session permet d'obtenir une instance de la classe encapsulant les données de l'occurrence de la base dont l'identifiant est fourni en paramètre.

Il existe deux surcharges de la méthode :

  • la première attend en premier paramètre le type de la classe des données et renvoie une nouvelle instance de cette classe
  • la seconde attend en paramètre une instance de la classe des données et la met à jour avec les données retrouvées
Exemple :
import net.sf.hibernate.*; 
import net.sf.hibernate.cfg.Configuration; 

public class TestHibernate2 { 
  
  public static void main(String args[]) throws Exception { 
    Configuration config = new Configuration(); 
    config.addClass(Personnes.class); 
    SessionFactory sessionFactory = config.buildSessionFactory(); 
    Session session = sessionFactory.openSession(); 
    
    try { 
      Personnes personne = (Personnes) session.load(Personnes.class, new Integer(3)); 
      System.out.println("nom = " + personne.getNomPersonne()); 
    } finally { 
      session.close(); 
    } 
    
    sessionFactory.close(); 
  } 
} 

Résultat :
C:\java\test\testhibernate>ant TestHibernate2                        
Buildfile: build.xml                                                 
                                                                     
init:                                                                
                                                                     
compile:                                                             
    [javac] Compiling 1 source file to C:\java\test\testhibernate\bin
                                                                     
TestHibernate2:                                                      
     [java] nom = nom3                                               
                                                                     
BUILD SUCCESSFUL                                                     
Total time: 9 seconds

 

56.7. L'obtention de données

Hibernate utilise plusieurs moyens pour obtenir des données de la base de données :

  • Hibernate Query Language (HQL)
  • API Criteria : Query By Criteria (QBC) et Query BY Example (QBE)
  • Requêtes SQL natives

 

56.7.1. Le langage de requête HQL

Pour offrir un langage d'interrogation commun à toutes les bases de données, Hibernate propose son propre langage nommé HQL (Hibernate Query Language).

L'intérêt de HQL est d'être indépendant de la base de données sous jacente : la requête SQL sera générée par Hibernate à partir du HQL en fonction de la base de données précisée via un dialect.

Hibernate Query Language (HQL) est un langage de requêtes orienté objets qui permet de représenter des requêtes SQL : les entités utilisées dans les requêtes HQL sont des objets et des propriétés. La syntaxe de HQL et ses fonctionnalités de base sont très similaire à SQL.

Il est aussi possible d'utiliser l'API Criteria qui va en interne exécuter une requête HQL. Le point d'entrée est d'obtenir une instance de type Criteria en invoquant la méthode createCriteria() de la session Hibernate courante : elle attend en paramètre la classe des objets attendus en résultat. L'API permet de préciser les différents critères qui seront utilisés pour générer la requête. L'API Criteria utilise HQL en sous-jacent.

Hibernate propose aussi un support pour exécuter des requêtes natives. Ceci permet d'utiliser des fonctionnalités de la base de données sous-jacente qui ne soient pas supportées par HQL. Cependant dans ce cas, le support multi-base de données offert par HQL sera probablement compromis.

Le langage HQL est proche de SQL avec une utilisation sous forme d'objets des noms de certaines entités : il n'y a aucune référence aux tables ou aux champs car ceux-ci sont référencés respectivement par leur classe et leurs propriétés. C'est Hibernate qui se charge de générer la requête SQL à partir de la requête HQL en tenant compte du contexte (type de base de données utilisée défini dans le fichier de configuration et la configuration du mapping).

 

56.7.1.1. La syntaxe de HQL

HQL possède une syntaxe similaire de celle de SQL : la différence majeure est que HQL utilise des objets et leurs propriétés alors que SQL utilise des tables et leurs colonnes

Exception faite des noms de classes et de variables, les requêtes HQL ne sont pas sensibles à la casse. Généralement les mots clé HQL sont en minuscule pour faciliter leur lecture.

Une requête HQL peut être composée :

  • de clauses
  • de fonctions d'agrégation
  • de sous requêtes

Les clauses sont les mots clés HQL qui sont utilisés pour définir la requête :

Clause

Description

Syntaxe

Exemple

from

précise la classe d'objets dont les occurrences doivent être retrouvées. Il est possible de définir un alias pour un objet en utilisant le mot clé alias

from object [as objectalias]

from Personne as pers (retourne toutes les occurrences de type Personne)

select

précise les propriétés à renvoyer. Doit être utilisé avec une clause from

 

select pers.nom from Personne as pers (retourne le nom de toutes les personnes)

where

précise une condition qui permet de filtrer les occurrences retournées. Doit être utilisé avec une clause select et/ou from

where condition

from Personne as pers where pers.nom = "Dupond" (retourne toutes les personnes dont le nom est Dupond.

order by

précise un ordre de tri sur une ou plusieurs propriétés. L'ordre par défaut est ascendant

order by propriete [asc|desc] [, propriete] ...;

select pers.nom, pers.prenom from Personne as pers order by pers.nom asc, pers.prenom desc

group by

précise un critère de regroupement pour les résultats retournés. Doit être utilisé avec une clause select et/ou from

group by propriete [, propriete] ...

 

Les fonctions d'agrégation HQL ont un rôle similaire à celles de SQL : elles permettent de calculer des valeurs agrégeant des valeurs de propriétés issues du résultat de la requête.

Fonction

Syntaxe

count

count( [distinct|all|*] object | object.property )

sum

sum( [distinct|all] object.property )

avg

avg( [distinct|all] object.property )

max

max( [distinct|all] object.property )

min

min( [distinct|all] object.property )


Résultat :
select avg(emp.salaire) from Employe as emp 

Les sous requêtes sont des requêtes imbriquées dans une autre requête

L'utilisation de sous requêtes dans HQL est conditionné par le support des sous requêtes par la base de données sous-jacente. Les sous requêtes sont entourées par des parenthèses : elles sont exécutées avant la requête principale puisque celle-ci a besoin des résultats pour son exécution.

Résultat :
from Employe as emp where emp.salaire >= (select avg(Employe.salaire)

from Employe)

 

56.7.1.2. La mise en oeuvre de HQL

La mise en oeuvre de HQL peut se faire de plusieurs manières.

Le plus courant est d'obtenir une instance de la classe Query en invoquant la méthode createQuery() de la session Hibernate courante : elle attend en paramètre la requête HQL qui devra être exécutée.

Exemple :
Session session;
Query query = session.createQuery("select pers.nom from Personne as pers");
List result = query.list();

La méthode list() de la classe Query permet d'obtenir une collection qui contient les résultats de l'exécution de la requête.

Il est également possible de définir des requêtes utilisant des paramètres nommés grâce à un objet implémentant l'interface Query. Dans ces requêtes, les paramètres sont précisés avec un caractère « : » suivi d'un nom unique.

L'interface Query propose de nombreuses méthodes setXXX() pour associer à chaque paramètre une valeur en fonction du type de la valeur (XXX représente le type). Chacune de ces méthodes possède deux surcharges permettant de préciser le paramètre (à partir de son nom ou de son index dans la requête) et sa valeur.

Pour parcourir la collection des occurrences trouvées, l'interface Query propose la méthode list() qui renvoie une collection de type List ou la méthode iterate() qui renvoie un itérateur sur la collection.

Exemple :
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
import java.util.*;

public class TestHibernate8 {
  
  public static void main(String args[]) throws Exception {
    SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
    Session session = sessionFactory.openSession();
    
    try {
      Query query = session.createQuery("from Personnes p where p.nomPersonne = :nom");
      query.setString("nom", "nom2");
      Iterator personnes = query.iterate();
      
      while (personnes.hasNext()) {
        Personnes personne = (Personnes) personnes.next();
        System.out.println("nom = " + personne.getNomPersonne());
      }
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate8
Buildfile: build.xml

init:
     [copy] Copying 1 file to C:\java\test\testhibernate\bin

compile:

TestHibernate8:
     [java] nom = nom2

BUILD SUCCESSFUL
Total time: 7 seconds

La méthode find() de la classe Session permet d'effectuer une recherche d'occurrences grâce à la requête fournie en paramètre.

Exemple : rechercher toutes les occurrences d'une table
import net.sf.hibernate.*; 
import net.sf.hibernate.cfg.Configuration; 
import java.util.*; 

public class TestHibernate3 { 
  
  public static void main(String args[]) throws Exception { 
    Configuration config = new Configuration(); 
    config.addClass(Personnes.class); 
    SessionFactory sessionFactory = config.buildSessionFactory(); 
    Session session = sessionFactory.openSession(); 

    try { 
      List personnes = session.find("from Personnes"); 
      for (int i = 0; i < personnes.size(); i++) { 
        Personnes personne = (Personnes) personnes.get(i); 
        System.out.println("nom = " + personne.getNomPersonne()); 
      } 
    } finally { 
      session.close(); 
    } 
    sessionFactory.close(); 
  } 
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate3
Buildfile: build.xml

init:

compile:
    [javac] Compiling 1 source file to C:\java\test\testhibernate\bin

TestHibernate3:
     [java] nom = nom1
     [java] nom = nom2
     [java] nom = nom3

BUILD SUCCESSFUL
Total time: 14 seconds

La méthode find() possède deux surcharges pour permettre de fournir un seul ou plusieurs paramètres dans la requête.

La première surcharge permet de fournir un seul paramètre : elle attend en paramètre la requête, la valeur du paramètre et le type du paramètre.

Exemple :
import net.sf.hibernate.*; 
import net.sf.hibernate.cfg.Configuration; 
import java.util.*; 

public class TestHibernate4 { 
 
  public static void main(String args[]) throws Exception { 
    Configuration config = new Configuration(); 
    config.addClass(Personnes.class); 
    SessionFactory sessionFactory = config.buildSessionFactory(); 
    Session session = sessionFactory.openSession(); 

    try { 
      List personnes = session.find("from Personnes p where p.nomPersonne=?",
        "nom1", Hibernate.STRING); 
      for (int i = 0; i < personnes.size(); i++) {  
        Personnes personne = (Personnes) personnes.get(i); 
        System.out.println("nom = " + personne.getNomPersonne()); 
      } 
    } finally { 
      session.close(); 
    } 
  
    sessionFactory.close(); 
  } 
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate4
Buildfile: build.xml

init:

compile:
    [javac] Compiling 1 source file to C:\java\test\testhibernate\bin

TestHibernate4:
     [java] nom = nom1

BUILD SUCCESSFUL

Dans la requête du précédent exemple, un alias nommé « p » est défini pour la classe Personnes. Le mode de fonctionnement d'un alias est similaire en HQL et en SQL.

La classe Session propose une méthode iterate() dont le mode de fonctionnement est similaire à la méthode find() mais elle renvoie un itérateur (objet de type Iterator) sur la collection des éléments retrouvés plutôt que la collection elle même.

Exemple :
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
import java.util.*;

public class TestHibernate6 {
  
  public static void main(String args[]) throws Exception {
    Configuration config = new Configuration();
    config.addClass(Personnes.class);
    SessionFactory sessionFactory = config.buildSessionFactory();
    Session session = sessionFactory.openSession();

    try {
      Iterator personnes = session.iterate("from Personnes ");
      while (personnes.hasNext()) {
        Personnes personne = (Personnes) personnes.next();
        System.out.println("nom = " + personne.getNomPersonne());
      } 
    } finally {
      session.close();
    }
    
    sessionFactory.close();
  }
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate6
Buildfile: build.xml

init:

compile:
    [javac] Compiling 1 source file to C:\java\test\testhibernate\bin

TestHibernate6:
     [java] nom = nom1
     [java] nom = nom2
     [java] nom = nom3

BUILD SUCCESSFUL

Il est aussi possible d'utiliser la clause « order by » dans une requête HQL pour définir l'ordre de tri des occurrences.

Exemple :

List personnes = session.find("from Personnes p order by p.nomPersonne desc");

Il est possible d'utiliser des fonctions telles que count() pour compter le nombre d'occurrences.

Exemple :
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
import java.util.*;

public class TestHibernate5 {

  public static void main(String args[]) throws Exception {
    Configuration config = new Configuration();
    config.addClass(Personnes.class);
    SessionFactory sessionFactory = config.buildSessionFactory();
    Session session = sessionFactory.openSession();

    try {
      int compteur = ( (Integer) session.iterate(
        "select count(*) from Personnes").next() ).intValue();
      System.out.println("compteur = " + compteur);
    } finally {
      session.close();
    }
    
    sessionFactory.close();
  }
}

Résultat :
C:\java\test\testhibernate>ant TestHibernate5
Buildfile: build.xml

init:

compile:
    [javac] Compiling 1 source file to C:\java\test\testhibernate\bin

TestHibernate5:
     [java] compteur = 3

BUILD SUCCESSFUL

Hibernate propose également d'externaliser une requête dans le fichier de mapping.

 

56.7.2. L'API Criteria

En plus du langage HQL pour réaliser des requêtes d'extraction de données, Hibernate propose une API qui permet de construire des requêtes pour interroger la base de données. L'API Criteria d'Hibernate propose donc une alternative à HQL sous la forme d'une API.

Le HQL possède une syntaxe dérivée de celle de SQL dans laquelle les notions relationnelles sont remplacées par des notions objets. Ceci oblige les développeurs à utiliser une syntaxe proche de celle du SQL.

L'API Criteria Query propose des objets pour définir les critères d'une requête ce qui permet aux développeurs de les définir d'une manière orientée objet plutôt que d'utiliser le HQL.

L'API Criteria propose donc d'avoir une approche orientée objet pour définir des requêtes et obtenir des données. L'utilisation de cette API permet d'avoir un meilleur contrôle grâce à la compilation.

Cette API permet de facilement combiner de nombreux critères optionnels pour créer une requête : elle est particulièrement adaptée pour créer dynamiquement des requêtes à la volée comme c'est le cas par exemple pour des requêtes effectuant des recherches multicritères à partir d'informations fournies par l'utilisateur.

Elle offre pour la plupart des fonctionnalités une approche bi directionnelle :

  • appliquer un critère en désignant la propriété sur laquelle il s'applique
  • appliquer un critère sur une propriété

Elle propose des classes et des interfaces qui encapsulent les fonctionnalités de SQL dont les principales sont :

  • Criteria
  • Criterion
  • Restrictions
  • Projection
  • Order

L'interface org.hibernate.criterion.Criteria est le point d'entrée pour utiliser l'API Criteria. Elle permet de définir une requête à partir de critères pour retrouver des données.

Exemple :
select * from Personne

Voici le code équivalent avec l'API Criteria :

Exemple :
      List personnes = session.createCriteria(Personne.class).list(); 

Les critères de recherche permettant de restreindre les données retournées par la requête sont définis par l'interface org.hibernate.criterion.Criterion. Le type Criterion encapsule un élément de la clause "where" de la requête SQL qui sera générée.

Exemple :
      Criteria criteria = session.createCriteria(Personne.class);
      Criterion critere = Restrictions.eq("id", 2l);
      criteria.add(critere);
      List personnes = criteria.list();

      System.out.println("nb personnes = " + personnes.size());
      Iterator it = personnes.iterator();
      while (it.hasNext()) {
        Personne personne = (Personne) it.next();
        System.out.println("Personne : " + personne);
      }

La classe org.hibernate.criterion.Restrictions est une fabrique qui propose des méthodes statiques pour créer des instances de type Criterion

Depuis la version 3.x d'Hibernate, il est préférable d'utiliser la classe Restrictions à sa classe fille Expressions.

L'interface org.hibernate.criterion.Projection encapsule un champ en réponse de la requête (un champ dans la clause "select" de la requête SQL).

La classe org.hibernate.criterion.Projections est une fabrique pour les instances de type Projection.

Exemple :
SELECT nom FROM Personne 

Exemple :
    List noms = session.createCriteria(Personne.class)
           .setProjection(Projections.property("nom"))
           .list();
     
    System.out.println("nb personnes = " + noms.size());
     
    Iterator it = noms.iterator();  
    while (it.hasNext()) { 
      String nom = (String) it.next();
      System.out.println("Personne : " + nom);
    }

La classe Order encapsule une clause SQL "order by".

 

56.7.2.1. L'utilité de HQL et de l'API Criteria

L'API Criteria permet de définir à la volée des recherches de données multicritères complexes. Un exemple typique d'utilisation où cette API est particulièrement utile est la recherche de données multicritères où elle va permettre de facilement construire dynamiquement les critères à appliquer en fonction de ceux précisés par l'utilisateur de l'application.

Ceci est d'autant plus vrai que le nombre de critères optionnels est important : la génération dynamique peut alors devenir particulièrement complexe surtout si elle implique des jointures conditionnées par les critères précisés.

L'approche traditionnelle consiste à construire dynamiquement la requête HQL par concaténation des chaînes de caractères qui correspondent à chaque critère renseigné avec généralement plusieurs problématiques à gérer :

  • la gestion des opérateurs logiques notamment pour l'omettre dans le cas du premier critère
  • la gestion des jointures à utiliser (ajout des classes et des critères de jointures)
  • l'utilisation directe des données dans la requête générée (pouvant par exemple conduire à des failles de sécurité dans les applications web)
Exemple :
  public List rechercher(Session session,
                                String nom,
                                String prenom,
                                Date dateDeb,
                                Date dateFin,
                                Integer taille) {
    Map<String, Object> parametres = new HashMap<String, Object>();
    boolean premiereClause = true;
    StringBuffer requeteBuffer = new StringBuffer("from Personne p ");

    if (nom != null) {
      requeteBuffer.append(premiereClause ? "where " : " and ");
      requeteBuffer.append("p.nom = :nom");
      parametres.put("nom", nom);
      premiereClause = false;
    }
    if (prenom != null) {
      requeteBuffer.append(premiereClause ? " where " : " and ");
      requeteBuffer.append("p.prenom = :prenom");
      parametres.put("prenom", prenom);
      premiereClause = false;
    }
    if (dateDeb != null) {
      requeteBuffer.append(premiereClause ? " where " : " and ");
      requeteBuffer.append("p.dateNais >= :dateDeb");
      parametres.put("dateDeb", dateDeb);
      premiereClause = false;
    }
    if (dateFin != null) {
      requeteBuffer.append(premiereClause ? " where " : " and ");
      requeteBuffer.append("p.dateNais <= :dateFin");
      parametres.put("endDate", dateFin);
      premiereClause = false;
    }
    if (taille != null) {
      requeteBuffer.append(premiereClause ? " where " : " and ");
      requeteBuffer.append("p.taille = :taille");
      parametres.put("taille", taille);
      premiereClause = false;
    }

    String requeteHql = requeteBuffer.toString();

    Query query = session.createQuery(requeteHql);

    Iterator<String> iter = parametres.keySet().iterator();
    while (iter.hasNext()) {
      String name = iter.next();
      Object value = parametres.get(name);
      query.setParameter(name, value);
    }

    return query.list();
  } 

Cette approche est lourde et source d'erreurs car le code contient des portions similaires répétées. Il est aussi très important de ne pas concaténer directement des valeurs saisies par l'utilisateur dans la requête.

L'API Criteria propose une solution plus propre, plus concise et plus sûre.

Exemple :
  public List rechercher(Session session,
                                String nom,
                                String prenom,
                                Date dateDeb,
                                Date dateFin,
                                Integer taille) {
    Criteria criteria = session.createCriteria(Personne.class);
    if (dateDeb != null) {
      criteria.add(Restrictions.ge("dateNais", dateDeb));
    }
    if (dateFin != null) {
      criteria.add(Restrictions.le("dateNais", dateFin));
    }
    if (nom != null) {
      criteria.add(Restrictions.eq("nom", nom));
    }
    if (prenom != null) {
      criteria.add(Restrictions.eq("prenom", prenom));
    }
    if (taille != null) {
      criteria.add(Restrictions.eq("taille", taille));
    }
    List resultat = criteria.list();

    return resultat;
  } 

La quantité de code nécessaire en utilisant l'API Criteria est beaucoup moins importante que la quantité nécessaire à la construction dynamique de la requête HQL.

 

56.7.2.2. L'interface Criteria

L'interface org.hibernate.Criteria propose des fonctionnalités pour encapsuler une requête composée de critères.

Une instance de l'interface Criteria est obtenue en invoquant la méthode createCriteria() de la session hibernate. Elle attends en paramètre la classe d'une entité sur laquelle les critères vont s'appliquer.

L'interface Criteria propose différentes méthodes pour construire les critères d'interrogation sur une classe persistante.

Méthode

Rôle

Criteria add(Criterion criterion)

ajouter un critère Criteria

addOrder(Order order)

ajouter un ordre de tri

Criteria createAlias(String associationPath, String alias)

ajouter une jointure en lui assignant un alias

Criteria createAlias(String associationPath, String alias, int joinType)

créer un objet de type Criteria pour une entité donnée en précisant son type et en lui assignant un alias

Criteria createCriteria(String associationPath)

créer un objet de type Criteria pour une entité donnée

Criteria createCriteria(String associationPath, int joinType)

créer un objet de type Criteria pour une entité donnée en précisant son type de jointure

Criteria createCriteria(String associationPath, String alias)

créer un objet de type Criteria pour une entité donnée en lui assignant un alias

Criteria createCriteria(String associationPath, String alias, int joinType)

créer un objet de type Criteria pour une entité donnée en précisant son type, en lui assignant un alias et en précisant son type de jointure

String getAlias()

obtenir l'alias de l'entité encapsulée dans le Criteria

List list()

obtenir les résultats de la requête

ScrollableResults scroll()

obtenir les résultats comme une instance de type ScrollableResults

ScrollableResults scroll(ScrollMode scrollMode)

obtenir les résultats comme une instance de type ScrollableResults en précisant le mode de parcours

Criteria setCacheable(boolean cacheable)

activer ou non la mise en cache des résultats de la requête

Criteria setCacheMode(CacheMode cacheMode)

modifier le mode de mise en cache des résultats de la requête

Criteria setCacheRegion(String cacheRegion)

préciser le nom de la région du cache à utiliser pour stocker les résultats de la requête

Criteria setComment(String comment)

ajouter un commentaire à la requête SQL

Criteria setFetchMode(String associationPath, FetchMode mode)

préciser le mode de récupération des données dans le cas d'une association entre deux entités

Criteria setFetchSize(int fetchSize)

préciser le nombre d'occurrences retournées par la requête

Criteria setFirstResult(int firstResult)

préciser la première occurrence qui sera retournée

Criteria setFlushMode(FlushMode flushMode)

préciser le mode de flush de la requête

Criteria setLockMode(LockMode lockMode)

préciser le mode de verrou de l'entité

Criteria setLockMode(String alias, LockMode lockMode)

préciser le mode de verrou pour l'entité dont l'alias est fourni en paramètre

Criteria setMaxResults(int maxResults)

assigner un nombre maximum d'occurrences retournées dans le résultat

Criteria setProjection(Projection projection)

préciser le contenu du résultat de la requête

Criteria setResultTransformer(ResultTransformer resultTransformer)

préciser une stratégie de traitement des résultats de la requête

Criteria setTimeout(int timeout)

assigner un timeout à la requête

Object uniqueResult()

ne renvoyer qu'une seule instance en résultat de la requête ou null si la requête ne renvoie aucun résultat.


Exemple :
      Criteria criteria = session.createCriteria(Personne.class);
      criteria.setMaxResults(10);
      List personnes = criteria.list();

 

56.7.2.3. L'interface Criterion

L'interface org.hibernate.criterion.Criterion définit les méthodes qui vont permettre de définir un critère à appliquer dans la requête.

Chaque instance d'un critère doit être ajoutée aux critères de la requête en utilisant la méthodes add() de l'instance de type Criteria.

L'API propose plusieurs fabriques qui permettent d'instancier les différents objets qui vont définir le contenu de la requête.

Exemple :
      List personnes = session.createCriteria(Personne.class)
                    .add(Restrictions.like("nom", "Dup%"))
                    .add(Restrictions.gt("taille", new Integer(180)))
                    .addOrder(Order.asc("age"))
                    .list();

La classe org.hibernate.criterion.Restrictions est une fabrique qui propose des méthodes pour obtenir différentes instances de Criterion.

Exemple :
      Calendar cal = Calendar.getInstance();
      cal.set(1980, Calendar.JANUARY, 01);
      Date dateDeb = cal.getTime();
      cal.set(1980, Calendar.DECEMBER, 31);
      Date dateFin = cal.getTime();
      personnes = session.createCriteria(Personne.class)
                         .add(Restrictions.ge("dateNais", dateDeb))
                         .add(Restrictions.le("dateNais", dateFin))
                         .addOrder(Order.asc("dateNais"))
                         .setFirstResult(0)
                         .setMaxResults(10)
                         .list();

L'interface Criterion possède de nombreuses interfaces filles : AbstractEmptinessExpression, BetweenExpression, Conjunction, Disjunction, EmptyExpression, Example, ExistsSubqueryExpression, IdentifierEqExpression, IlikeExpression, InExpression, Junction, LikeExpression, LogicalExpression, NaturalIdentifier, NotEmptyExpression, NotExpression, NotNullExpression, NullExpression, PropertyExpression, PropertySubqueryExpression, SimpleExpression, SimpleSubqueryExpression, SizeExpression, SQLCriterion, SubqueryExpression.

 

56.7.2.4. Les restrictions et les expressions

La classe org.hibernate.criterion.Restrictions permet de créer des critères qui sont des conditions permettant de sélectionner les données à retrouver.

La classe Restrictions est une fabrique qui permet de créer des critères de recherche sous la forme d'instances de type Criterion. Les critères proposés encapsulent les opérateurs SQL standards.

Elle propose des méthodes statiques pour créer des instances des différentes implémentations de l'interface Criterion proposées par Hibernate. Ces critères sont utilisés pour définir les occurrences qui seront retournées par le résultat de la requête.

Exemple :
SELECT * FROM Personne WHERE IdPers=1;

Exemple :
      List personnes = session.createCriteria(Personne.class)
                              .add(Restrictions.eq("id", 1l))
                              .list();

Les conditions standards de SQL sont encapsulées dans des objets de type Criterion : pour obtenir une de leurs instances, il faut utiliser les méthodes statiques de la classe Restrictions dont les principales sont :

Méthode

Rôle

Criterion allEq(Map properties)

Méthode utilitaire qui permet de facilement vérifier que plusieurs propriétés ont une valeur particulière. Les clés du paramètre de type Map correspondent aux noms des propriétés concernées

LogicalExpression and(Criterion lhs, Criterion rhs)

Créer un critère de type "and" qui est vrai si les deux critères sont évalués à vrai

Criterion between(String propertyName, Object lo, Object hi)

Permet d'appliquer une contrainte SQL de type "between" : la valeur de la propriété dont le nom est fourni en paramètre doit être comprise entre les deux valeurs fournies

Conjunction conjunction()

Créer un objet de type Conjunction qui permet d'utiliser un critère de type and simplement en invoquant sa méthode add() pour chaque critère à prendre en compte. Le critère encapsulé dans l'objet de type Conjunction sera true si tous les critères qu'il contient sont true

Disjunction disjunction()

Créer un objet de type Disjunction qui permet d'utiliser un critère de type or simplement en invoquant sa méthode add() pour chaque critère à prendre en compte. Le critère encapsulé dans l'objet de type Disjunction sera true si au moins un des critères qu'il contient est true

SimpleExpression eq(String propertyName, Object value)

Permet d'appliquer une contrainte SQL de type égalité : la valeur de la propriété doit être égale à la valeur fournie en paramètre

PropertyExpression eqProperty(String propertyl, String property2)

La valeur des deux propriétés doit être égale

SimpleExpression ge(String propertyName, Object value)

Permet d'appliquer une contrainte SQL de type "supérieur ou égal" : la valeur de la propriété doit être supérieure ou égale à la valeur fournie en paramètre

PropertyExpression geProperty(String propertyl, String property2)

La valeur de la propriété doit être supérieure ou égale à la valeur de la seconde propriété fournie en paramètre

SimpleExpression gt(String propertyName, Object value)

Permet d'appliquer une contrainte SQL de type "supérieur à" : la valeur de la propriété doit être supérieure à la valeur fournie en paramètre

PropertyExpression gtProperty(String propertyl, String property2)

La valeur de la propriété doit être supérieure à la valeur de la seconde propriété fournie en paramètre

Criterion idEq(Object value)

permet d'appliquer une contrainte SQL de type égalité sur l'identifiant : la valeur de la propriété qui est l'identifiant doit être égale à celle fournie

Criterion ilike(String property, Object value)

Joue le même rôle que la méthode like() mais en étant insensible à la casse

Criterion ilike(String property, String value, MatchMode mode)

Joue le même rôle que la méthode like() mais en étant insensible à la casse et sans utiliser la syntaxe de l'opérateur like. MatchMode est une énumération qui peut prendre les valeurs START, END, ANYWHERE, ou EXACT.

Criterion in(String property, Collection values)

La valeur de la propriété dont le nom est fourni en paramètre doit être égale à l'une de celles fournies dans la collection

Criterion in(String propertyName, Collection values)

Permet d'appliquer une contrainte SQL de type "in" : la valeur de la propriété dont le nom est fourni en paramètre doit être égale à l'une de celles fournies dans le tableau

Criterion isEmpty(String property)

Le contenu de la collection de la propriété dont le nom est fourni en paramètre ne doit pas avoir d'éléments

Criterion isNotEmpty(String property)

Le contenu de la collection de la propriété dont le nom est fourni en paramètre doit avoir au moins un élément

Criterion isNotNull(String propertyName)

Permet d'appliquer une contrainte SQL de type "is not null" : la valeur de la propriété dont le nom est fourni en paramètre doit être non null

Criterion isNull(String propertyName)

Permet d'appliquer une contrainte SQL de type "is null" : la valeur de la propriété dont le nom est fourni en paramètre doit être null

SimpleExpression le(String property, Object value)

La valeur de la propriété dont le nom est fourni en paramètre doit être inférieure ou égale à la valeur fournie

PropertyExpression leProperty(String propertyl, String property2)

La valeur de la propriété fournie en paramètre doit être inférieure ou égale à la valeur fournie

SimpleExpression like(String property, Object value)

La valeur de la propriété dont le nom est fourni en paramètre doit respecter le motif de l'opérateur sql like fourni en paramètre

SimpleExpression like(String property, String value, MatchMode mode)

La valeur de la propriété dont le nom est fourni en paramètre doit respecter le motif de l'opérateur sql like déterminé à partir des paramètre valeur et mode. MatchMode est une énumération qui peut prendre les valeurs START, END, ANYWHERE, ou EXACT.

SimpleExpression lt(String property, Object value)

La valeur de la propriété doit être inférieure à la valeur fournie en paramètre

PropertyExpression ltProperty(String propertyl, String property2)

La valeur de la première propriété doit être inférieure à la seconde

SimpleExpression ne(String propertyName, Object value)

Permet d'appliquer une contrainte SQL de type "est différent de" : la valeur de la propriété dont le nom est fourni en paramètre doit être différente de la valeur fournie

PropertyExpression neProperty(String propertyName, String otherPropertyName)

La valeur des deux propriétés fournies en paramètres doit être différente

Criterion not(Criterion expression)

L'évaluation du critère fourni en paramètre doit être false

LogicalExpression or(Criterion lhs, Criterion rhs)

L'évaluation d'un des deux critères fourni en paramètre doit être true

Criterion sqlRestriction(String sql)

Appliquer une restriction en sql natif

Criterion sqlRestriction(String sql, Object[] values, Type[] types)

Appliquer une restriction en sql natif qui va utiliser les paramètres fournis

Criterion sqlRestriction(String sql, Object value, Type type)

Appliquer une restriction en sql natif qui va utiliser le paramètre fourni


Certaines de ces méthodes attendent un paramètre de type Criterion qui permet de faire des combinaisons de critères.

Les opérateurs de comparaison sont encapulés dans des méthodes de la classe Restrictions : eq(), lt(), le(), gt(), ge().

Exemple :
      List personnes = session.createCriteria(Personne.class)                    
                              .add(Restrictions.lt("dateNais", dateSaisie))
                              .list();

La classe Restrictions propose aussi des méthodes pour les opérateurs SQL : like, between, in, is null, is not null ...

Exemple :
      List personnes = session.createCriteria(Personne.class)
                              .add(Restrictions.between("dateNais", dateDeb, dateFin))
                              .add(Restrictions.like("nom", "Dup%"))
                              .list();

La classe Restrictions propose aussi des méthodes qui permettent de faire des comparaisons entre propriétés.

Exemple :
      List personnes = session.createCriteria(Personne.class)
                              .add(Restrictions.eqProperty("nom", "prenom"))
                              .list();

L'utilisation de la méthode sqlRestriction() peut être très pratique mais elle peut nuire à la portabilité entre bases de données.

Pour préciser l'alias de la table courante dans la portion de requête SQL fournie en paramètre de la méthode sqlRestriction(), il faut utiliser la syntaxe "{alias}".

Il est possible d'utiliser les méthodes or() et and() pour réaliser des combinaisons de critères.

Exemple :
      List personnes = session.createCriteria(Personne.class)
                              .add(Restrictions.or(Restrictions.eq("prenom", "Jean"),
                                   Restrictions.eq("prenom", "Paul")))
                              .list();

Remarque : il est préférable d'utiliser la classe Restrictions plutôt que sa classe fille Expressions qui est deprecated.

 

56.7.2.5. Les projections et les aggregations

La classe org.hibernate.criterion.Projection permet de préciser un champ qui sera retourné dans le résultat de la requête : ce champ peut être issu d'une table, du calcul d'une aggrégation, de la définition d'un alias, ...

Pour ajouter un champ, il faut passer le nom du champ en paramètre de la méthode statique property() de la classe Projection. L'instance retournée est passée en paramètre de la méthode setProjection().

La classe org.hibernate.criterion.Projections est une fabrique pour créer des instances de type Projection.

Pour préciser plusieurs champs, il faut utiliser la méthode propertyList() de la classe ProjectionList.

Exemple :
SELECT NOM, PRENOM FROM PERSONNE 

Exemple :
      List resultats = session.createCriteria(Personne.class)
                .setProjection(Projections.projectionList()
                               .add(Projections.property("nom"))
                               .add(Projections.property("prenom")))
                .list();

      System.out.println("nb personnes = " + resultats.size());
      Iterator it = resultats.iterator();
      while (it.hasNext()) {
        Object[] donnees = (Object[]) it.next();
        System.out.println("Nom : " + donnees[0] 
                         + " Prenom : " + donnees[1]);
      } 

Pour appliquer une collection de type Projection aux critères de la requête, il faut utiliser la méthode setProjection() de la classe Criteria. Une collection d'objets de type Projection est encapsulée dans un objet de type ProjectionList. La méthode add() permet d'ajouter une Projection à la collection.

Exemple :
      List resultats = session.createCriteria(Personne.class)
                              .setProjection(Projections.rowCount())
                              .list();
      Long valeur = (Long) resultats.get(0);
      System.out.println("nb personnes = " + valeur);

Les données de certaines requêtes doivent parfois être groupées ou intervenir dans un calcul d'aggrégation : il faut pour cela utiliser les fonctionnalités encapsulées dans la classe Projections. Toutes les fonctions d'aggrégation de la classe Projections sont des méthodes statiques.

La classe Projections possède plusieurs méthodes statiques :

Méthode

Rôle

static Projections alias(Projection projection, String alias)

Assigner un alias

static AgregateProjection avg(String property)

Calculer la moyenne du champ dont le nom est fourni en paramètre

static CountProjection count(String property)

Calculer le nombre d'occurrences du champ dont le nom et fourni en paramètre

static CountProjection countDistinct(String property)

Calculer le nombre d'occurrences distinctes du champ dont le nom et fourni en paramètre

static Projection distinct(Projection projection)

Ne retourner que des valeurs uniques (supprimer les valeurs en doublon).

static PropertyProjection groupProperty(String property)

Grouper les résultats sur la propriété fournie

static IdentifierProjection id()

Renvoyer l'identifiant

static AggregateProjection max(String property)

Déterminer la plus grande valeur pour le champ dont le nom est fourni en paramètre

static AggregateProjection min(String property)

Déterminer la plus petite valeur pour le champ dont le nom est fourni en paramètre

static ProjectionList projectionList()

Retourner une collection de projections

static PropertyProjection property(String property)

Ajouter la propriété fournie en paramètre

static Projection rowCount()

Calculer le nombre d'occurrences retournées par la requête

static Projection sqlGroupProjection(String sql, String groupBy, String[] columnAliases, Type[] types)

Ajouter du code SQL spécifique à la base de données utilisée pour déterminer un groupage. Le paramètre groupBy peut contenir une clause GROUP BY

static Projection sqlProjection(String sql, String[] columnAliases, Type[] types)

Ajouter du code SQL spécifique à la base de données utilisée pour déterminer la liste de champs retournée par la requête

static AggregateProjection sum(String property)

Calculer la sommes des valeurs pour le champ dont le nom est fourni en paramètre


Lors de l'utilisation d'opérateurs d'aggrégation, il est fréquent de grouper les données par rapport à un champ particulier. La classe Projections prossède la méthode groupProperty() qui permet de définir une clause "group by" qui sera utilisée avec le nom du champ fourni en paramètre.

Exemple :
SELECT COUNT(ID) FROM PERSONNE GROUP BY TAILLE 

Exemple :
      List resultats = session.createCriteria(Personne.class)
                    .setProjection(Projections.projectionList()
                                              .add(Projections.count("id"))
                                              .add(Projections.groupProperty("taille")))
                    .list();

      Iterator it = resultats.iterator();
      while (it.hasNext()) {
        Object[] donnees = (Object[]) it.next();
        System.out.println("Nombre : " + donnees[0] + " taille : " + donnees[1]);
      } 

 

56.7.2.6. La classe Property

La classe org.hibernate.criterion.Property est une fabrique qui permet de créer des critères spécifiques appliqués à la propriété encapsulée. Ceci permet d'appliquer des critères directement sur une propriété.

La méthode statique forName() de la classe Property permet d'obtenir une instance qui encapsule une propriété.

La classe org.hibernate.criterion.Property propose plusieurs méthodes pour appliquer des critères sur la propriété qu'elle encapsule.

Les principales méthodes sont :

Méthode

Rôle

Order asc()

Trier les valeurs de la propriété dans un ordre ascendant

AggregateProjection avg()

Créer un champ qui calcule la moyenne des occurrences de la propriété

Criterion between(Object min, Object max)

Créer un critère qui requiert que la valeur de la propriété soit comprise entre les valeurs min et max fournies en paramètres

CountProjection count()

Créer un champ qui compte le nombre d'occurrences de la propriété

Order desc()

Trier les valeurs de la propriété dans un ordre descendant

SimpleExpression eq(Object value)

Créer un critère pour filtrer les résultats de façon à ne retourner que les occurrences dont la valeur de la propriété soit égale à celle fournie en paramètre

PropertyExpression eqProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit égale à celle de la valeur de la propriété fournie en paramètre

PropertyExpression eqProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit égale à celle de la valeur de la propriété dont le nom est fourni en paramètre

SimpleExpression ge(Object value)

Créer un critère qui requiert que la valeur de la propriété soit supérieure ou égale à celle fournie en paramètre

PropertyExpression geProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit supérieure ou égale à celle de la valeur de la propriété fournie en paramètre

PropertyExpression geProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit supérieure ou égale à celle de la valeur de la propriété dont le nom est fourni en paramètre

PropertyProjection group()

Demander un groupage sur les valeurs de la propriété

SimpleExpression gt(Object value)

Créer un critère qui requiert que la valeur de la propriété soit supérieure à celle fournie en paramètre

PropertyExpression gtProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit supérieure à celle de la valeur de la propriété fournie en paramètre

PropertyExpression gtProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit supérieure à celle de la valeur de la propriété dont le nom est fourni en paramètre

Criterion in(Collection values)

Créer un critère qui requiert que la valeur de la propriété doit être égale à une de celles fournies

in(Object[] values)

Créer un critère qui requiert que la valeur de la propriété doit être égale à une de celles fournies

Criterion isEmpty()

Créer un critère qui requiert que la valeur de la propriété ne possède aucun élément

Criterion isNotEmpty()

Créer un critère qui requiert que la valeur de la propriété possède au moins un élément

Criterion isNotNull()

Créer un critère qui requiert que la valeur de la propriété ne soit pas null

Criterion isNull()

Créer un critère qui requiert que la valeur de la propriété soit null

SimpleExpression le(Object value)

Créer un critère qui requiert que la valeur de la propriété soit inférieure ou égale à celle fournie en paramètre

PropertyExpression leProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit inférieure ou égale à celle de la valeur de la propriété fournie en paramètre

PropertyExpression leProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit inférieure ou égale à celle de la valeur de la paramètre dont le nom est fourni en paramètre

SimpleExpressionlike(Object value)

Créer un critère qui requiert que la valeur de la propriété respecte le motif fourni au sens de l'opérateur SQL like

like(String value, MatchMode mode)

Créer un critère qui requiert que la valeur de la propriété respecte un motif fourni au sens de l'opérateur SQL like Le motif est construit à partir de la sous chaîne fournie en paramètre et de son mode recherche qui peut prendre les valeurs START, END, ANYWHERE, and EXACT. Ceci permet d'éviter d'avoir à manipuler la syntaxe de l'opérateur SQL like.

SimpleExpression lt(Object value)

Créer un critère qui requiert que la valeur de la propriété soit supérieure ou égale à celle fournie en paramètre

PropertyExpression ltProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit supérieure ou égale à celle de la valeur de la propriété fournie en paramètre

PropertyExpression ltProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit inférieure ou égale à celle de la valeur de la paramètre dont le nom est fourni en paramètre

AggregateProjection max()

Créer un champ qui va contenir la valeur la plus élevée de la propriété

AggregateProjection min()

Créer un champ qui va contenir la valeur la moins élevée de la propriété

PropertyProjection ne(Object value)

Créer un critère qui requiert que la valeur de la propriété soit différente de celle fournie en paramètre

PropertyProjection neProperty(Property other)

Créer un critère qui requiert que la valeur de la propriété soit différente de celle de la valeur de la propriété fournie en paramètre

PropertyExpression neProperty(String property)

Créer un critère qui requiert que la valeur de la propriété soit différente de celle de la valeur de la paramètre dont le nom est fourni en paramètre

 

56.7.2.7. Le tri des résultats

Il est possible de demander le tri des résultats de la requête en utilisant la méthode addOrderO de la classe Criteria et la classe Order.

La classe org.hibernate.criterion.Order permet d'encapsuler une clause de tri dans la requête en précisant le sens (ascendant ou descendant) sur un champ.

Elle possède plusieurs méthodes :

Méthode

Rôle

static Order asc(String)

Demander un tri ascendant sur le nom du champ fourni en paramètre de la méthode

static Order desc(String)

Demander un tri descendant sur le nom du champ fourni en paramètre de la méthode

Order ignoreCase()

Demander d'ignorer la casse lors du tri des données


Exemple :
      List resultats = session.createCriteria(Personne.class)
        .add(Restrictions.between("dateNais",dateDeb,dateFin))
        .addOrder(Order.desc("dateNais"))
        .addOrder(Order.asc("nom"));

 

56.7.2.8. La jointure de tables

Il est très fréquent d'avoir à effectuer une ou plusieurs jointures sur les tables d'une requête pour obtenir les données souhaitées.

En SQL, la jointure se fait en utilisant les tables dans la clause from et en indiquant les conditions de la jointure.

Exemple :
SELECT P.*, A.* FROM Personne P, Adresse A WHERE P.adresse_id=A.id
AND A.Nom = 'Dupond'; 

En HQL, il est possible de charger les données d'objets dépendants en utilisant la clause "left join fetch" :

Exemple :
from Personne personne
where Personne.nom = :nom left join fetch personne.adresse 

La méthode setFetchMode() permet de faire une jointure avec l'API Criteria.

Exemple :
package com.jmdoudoux.test.hibernate;
      
import java.util.Iterator;
import java.util.List;
import org.hibernate.FetchMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;

public class TestHibernateCriteria {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new AnnotationConfiguration().configure()
                                                                 .buildSessionFactory();
    Transaction transaction = null;
    List personnes = null;
    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();
      personnes = session.createCriteria(Personne.class)
                         .setFetchMode("adresse", FetchMode.JOIN)
                         .list();
      System.out.println("nbpersonnes = " + personnes.size());

      Iterator it = personnes.iterator();
      while (it.hasNext()) {
        Personne personne = (Personne) it.next();
       
        System.out.println("Personne : " + personne);
        System.out.println("  Adresse : " + personne.getAdresse());
      }

      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }
    sessionFactory.close();
  }
}

La méthode setFetchMode() attend deux paramètres :

  • le nom de la classe de l'entité dont la table sera jointe avec celle de l'entité courante
  • le mode de récupération des données : DEFAULT (valeur configurée dans le mapping), EAGER (deprecated : utiliser JOIN), JOIN (récupération des données par jointure), LAZY (deprecated : utiliser SELECT), SELECT (récupération des données par une requête dédiée)

Parfois, il est nécessaire de joindre les entités mais il est inutile de récupérer les données de l'entité jointe : dans ce cas la jointure n'est utile que pour définir un ou plusieurs critères de la requête

Exemple :
 from Personne p join p.adresse a where a.cp = '54000' 

Il est possible d'utiliser la méthode createCriteria() pour créer une jointure :

Exemple :
package com.jmdoudoux.test.hibernate;
      
import java.util.Iterator;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.criterion.Restrictions;

public class TestHibernateCriteria {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new AnnotationConfiguration().configure()
                                                                 .buildSessionFactory();
   
    Transaction transaction = null;
    List personnes = null;
   
    Session session = sessionFactory.openSession();
    try {
      transaction = session.beginTransaction();
      personnes = session.createCriteria(Personne.class)
                         .createCriteria("adresse", "a")
                         .add(Restrictions.eq("a.cp", "54700"))
                         .list();
     
      System.out.println("nb personnes = " + personnes.size());
     
      Iterator it = personnes.iterator();
      while (it.hasNext()) {
        Personne personne = (Personne) it.next();
        System.out.println("Personne : " + personne);
        System.out.println("  Adresse: " + personne.getAdresse());
      }
     
      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }
   
    sessionFactory.close();
  }
}

Attention : dans ce cas, le premier paramètre de la méthode createCriteria() est le nom d'une propriété et non le nom de classe.

Il est aussi possible d'utiliser la méthode createAlias() qui évite d'avoir à créer une nouvelle instance de la classe Criteria.

Exemple :
      personnes = session.createCriteria(Personne.class)
                         .createAlias("adresse", "a")
                         .add(Restrictions.eq("a.cp", "54000"))
                         .list();

Les restrictions sont aussi utilisables avec une jointure dans laquelle les données de la table jointe sont récupérées.

Exemple :
SELECT G.*, P.* FROM Personnes P, Groupe G WHERE G.IdGroup=P.IdGroup AND G.IdGroup=l;

Exemple :
      List groupes = session.createCriteria(Groupe.class) 
                            .setFetchMode("Personne",FetchMode.JOIN) 
                            .add(Restrictions.eq("IdGroup","1"))
                            .list();

 

56.7.2.9. La création de critères à partir de données

La classe Example permet de créer des critères par l'exemple : pour cela, il faut instancier une entité et lui affecter les différentes valeurs qui seront utilisées comme critères.

La méthode static create() qui attend en paramètre une instance d'une entité va utiliser l'instrospection pour rechercher les propriétés qui possèdent une valeur et générer une instance de type Example contenant les critères correspondants aux propriétés renseignées.

Exemple :
      transaction = session.beginTransaction();
      Date dateDebut = new GregorianCalendar(1980, Calendar.JANUARY, 01).getTime();
      Date dateFin = new GregorianCalendar(1981, Calendar.JANUARY, 01).getTime();

      Personne personneExemple = new Personne();
      personneExemple.setNom("Dupond");
      personneExemple.setPrenom("Michel");
      Example example = Example.create(personneExemple).ignoreCase().excludeZeroes();
      List<Personne> resultats = session.createCriteria(Personne.class)
                                              .add(example)
                                              .add(Restrictions.between("dateNais",
                                                   dateDebut, dateFin)).list();
      System.out.println("nbpersonnes = " + personnes.size());
      
      Iterator it = personnes.iterator();
      while (it.hasNext()) {
        Personne personne = (Personne) it.next(); 
        System.out.println("Personne : " + personne);
        System.out.println("  Adresse : " + personne.getAdresse());
      }
      transaction.commit();

Il est possible de configurer le comportement de la classe Example en utilisant les différentes méthodes qu'elle propose à cette fin :

Méthode

Rôle

Example enableLike()

Utiliser l'opérateur like pour toutes les propriétés de type String

Example enableLike(MatchMode matchMode)

Utiliser l'opérateur like pour toutes les propriétés de type String avec la stratégie de correspondance fournie en paramètre

Example excludeNone()

Ne pas exclure les propriétés dont la valeur est null ou zéro

Example excludeProperty(String name)

Ignorer la propriété dont le nom est fourni en paramètre

Example excludeZeroes()

Ignorer les propriétés dont la valeur est zéro

Example ignoreCase()

Ignorer la casse des propriétés de type String

Example setEscapeCharacter(Character escapeCharacter)

Préciser le caractère d'échappement utilisé dans la clause like


Cette API est particulièrement utile si le nombre de propriétés est important. Comme la classe Example hérite de la classe Criterion, elle peut être utilisée pour créer les critères "simple" et utiliser d'autres classes pour les critères plus complexes ou spécifiques.

 

56.7.2.10. Le choix entre HQL et l'API Criteria

L'API Criteria est très puissante et elle est particulièrement bien adaptée pour certaines tâches (notamment la création de requêtes dynamiques à la volée impliquant de nombreux critères optionnels comme par exemple dans un formulaire de recherche multicritères).

Dans ces cas, sa mise en oeuvre peut permettre d'avoir un code plus propre, plus sûre et plus maintenable.

Cependant, son utilisation ne peut pas toujours être généralisée à tous les cas de figure car l'utilisation de HQL est parfois préférable, notamment si la requête n'est pas dynamique.

Il est par exemple préférable d'externaliser les requêtes HQL lorsque cela est possible ce qui présente plusieurs avantages :

  • faciliter de maintenance
  • faciliter pour recencer et optimiser les requêtes
  • faciliter pour mettre les requêtes en cache

Il est donc nécessaire de bien choisir entre HQL et l'API Criteria en fonction des besoins et de leur adéquation avec ce que proposent les deux solutions qui se recouvrent mais sont aussi complémentaires.

 

56.8. La mise à jour d'une occurrence

Pour mettre à jour une occurrence dans la source de données, il suffit d'appeler la méthode update() de la session en lui passant en paramètre l'objet encapsulant les données.

Le mode de fonctionnement de cette méthode est similaire à celui de la méthode save().

La méthode saveOrUpdate() laisse Hibernate choisir entre l'utilisation de la méthode save() ou update() en fonction de la valeur de l'identifiant dans la classe encapsulant les données.

 

56.9. La suppression d'une ou plusieurs occurrences

La méthode delete() de la classe Session permet de supprimer une ou plusieurs occurrences en fonction de la version surchargée de la méthode utilisée.

Pour supprimer une occurrence encapsulée dans une classe, il suffit d'invoquer la méthode delete() en lui passant en paramètre l'instance de la classe.

Pour supprimer plusieurs occurrences, voire toutes, il faut passer en paramètre de la méthode delete(), une chaîne de caractères contenant la requête HQL pour préciser les éléments concernés par la suppression.

Exemple : suppression de toutes les occurrences de la table
session.delete("from Personnes");

 

56.10. Les relations

Un des fondements du modèle de données relationnelles repose sur les relations qui peuvent intervenir entre une table et une ou plusieurs autres tables ou la table elle même.

Les relations utilisables dans le monde relationnel et le monde objet sont cependant différentes.

Les relations du monde objets possèdent quelques caractéristiques :

  • Elles sont réalisées via des références entre objets
  • Elles peuvent mettre en oeuvre l'héritage et le polymorphisme

Les relations du monde relationnel possèdent quelques caractéristiques :

  • Elles sont gérées par des clés étrangères et des jointures entre tables

Les caractéristiques de ces deux modèles sont assez différentes : le but d'un outil de type ORM comme Hibernate est de permettre de manipuler des entités objets et de masquer au développeur le monde relationnel en assurant un mapping entre les deux mondes. Cependant Hibernate n'assure pas en automatique la gestion inverse des relations qui reste à la charge du développeur.

Hibernate propose de transcrire les relations du modèle relationnel dans le modèle objet. Il supporte plusieurs types de relations :

  • relation de type 1 - 1 (one-to-one)
  • relation de type 1 - n (one-to-many)
  • relation de type n - n (many-to-many)

Dans le fichier de mapping, il est nécessaire de définir les relations entre la table concernée et les tables avec lesquelles elle possède des relations.

Les relations peuvent aussi être définies avec des annotations.

La version d'Hibernate utilisée dans cette section est la 3.5.1.

La base de données utilisée est une base MySQL version 4.1.9.

Chaque exemple possède plusieurs bibliothèques dans son classpath : commons-collections-3.1.jar, dom4j-1.6.1.jar, hibernate-jpa-2.0-api-1.0.0.Final.jar, hibernate3.jar, javassist-3.9.0.GA.jar, jta-1.1.jar, log4j-1.2.15.jar, mysql-connector-java-5.1.12-bin.jar, slf4j-api-1.5.8.jar, slf4j-log4j12-1.5.11.jar.

 

56.10.1. Les relations un / un

Dans ce type de relation, deux entités sont liées de façon à n'avoir qu'une seule et unique occurrence l'une pour l'autre.

Dans l'exemple ci-dessus, chaque personne ne peut avoir qu'une seule adresse et une adresse ne peut appartenir qu'à une seule personne.

Cette relation peut se traduire de plusieurs manières dans la base de données :

  • Une seule table qui contient les données de la personne et son adresse
  • Deux tables, une pour les personnes et une pour les adresses avec une clé primaire partagée
  • Deux tables, une pour les personnes et une pour les adresses avec une clé étrangère

Il y a plusieurs façons de traiter ce cas avec une ou deux tables dans la base de données et Hibernate :

  • Deux tables et une relation One-to-One d'Hibernate
  • Une seule table avec un Component d'Hibernate

 

56.10.1.1. Le mapping avec un Component

Comme une personne ne peut avoir qu'une seule adresse, il est préférable pour des raisons de performance de stocker les données des deux entités dans une seule et même table. Ceci évite d'avoir à faire une jointure lors de l'accès aux données des deux entités.

La description de la table personne est la suivante :

Résultat :
mysql> desc personne;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| Id         | int(11)      |      | PRI | NULL    | auto_increment |
| Nom        | varchar(255) |      |     |         |                |
| Prenom     | varchar(255) |      |     |         |                |
| DateNais   | date         | YES  |     | NULL    |                |
| ligne1_adr | varchar(80)  |      |     |         |                |
| ligne2_adr | varchar(80)  | YES  |     | NULL    |                |
| cp_adr     | varchar(5)   | YES  |     | NULL    |                |
| ville_adr  | varchar(80)  | YES  |     | NULL    |                |
| ligne3_adr | varchar(80)  | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)

Le script DDL correspondant est le suivant :

Résultat :
CREATE TABLE `personne` (
  `Id` int(11) NOT NULL auto_increment,
  `Nom` varchar(255) NOT NULL default '',
  `Prenom` varchar(255) NOT NULL default '',
  `DateNais` date default NULL,
  `ligne1_adr` varchar(80) NOT NULL default '',
  `ligne2_adr` varchar(80) default NULL,
  `cp_adr` varchar(5) default NULL,
  `ville_adr` varchar(80) default NULL,
  `ligne3_adr` varchar(80) default NULL,
  PRIMARY KEY  (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;

 

56.10.1.1.1. La configuration dans le fichier de mapping

Les classes qui encapsulent l'entité personne et les données de l'adresse sont de simples POJO.

Exemple :
package com.jmdoudoux.test.hibernate;

public class Personne {

  private Long id;
  private String nom;
  private String prenom;
  private String dateNais;
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }

  public String getPrenom() {
    return prenom;
  }

  public void setPrenom(String prenom) {
    this.prenom = prenom;
  }

  public String getDateNais() {
    return dateNais;
  }

  public void setDateNais(String dateNais) {
    this.dateNais = dateNais;
  }

  public Long getId() {
    return id;
  }

  // Attention le setter est requis par Hibernate
  public void setId(Long id) {
    this.id = id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

La classe Adresse ne possède pas de champ de type identifiant.

Exemple :
package com.jmdoudoux.test.hibernate;

public class Adresse {

  private String ligne1;
  private String ligne2;
  private String cp;
  private String ville;
  private String ligne3;

  public Adresse(String ligne1, String ligne2, String cp, String ville, String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }

  public Adresse() {
  }

  //
  // getter et setter sur les champs de la classe
  //
}

Les données de l'adresse sont encapsulées dans une classe Adresse : la définition des champs de cette classe est faite dans un élément de type component du fichier de mapping de l'entité Personne (Personne.hbm.xml).

Exemple :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.Personne" table="Personne">
    <id name="id" column="id">
      <generator class="increment" />
    </id>
    <property name="nom" column="Nom" />
    <property name="prenom" column="Prenom" />
    <property name="dateNais" column="DateNais" />
    <component name="adresse" class="com.jmdoudoux.test.hibernate.Adresse">
      <property name="ligne1" column="ligne1_adr" />
      <property name="ligne2" column="ligne2_adr" />
      <property name="cp" column="cp_adr" />
      <property name="ville" column="ville_adr" />
      <property name="ligne3" column="ligne3_adr" />
    </component>
  </class>
</hibernate-mapping>

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et le fichier de mapping de l'entité Personne.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
         
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property>
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping resource="com/jmdoudoux/test/hibernate/Personne.hbm.xml"></mapping>
  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type personne,
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class TestHibernate15 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new Configuration().configure()
                                                    .buildSessionFactory();
 
    Transaction transaction = null;
    int index = 6;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);

      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi dans la table Personne.

Résultat :
mysql> select * from personne;
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
| Id | Nom  | Prenom   | DateNais | ligne1_adr | ligne2_adr |
cp_adr | ville_adr
 | ligne3_adr |
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
|  1 | nom6 | prenom_6 | NULL     | ligne1_6   | ligne2_6  
| cp_6   | ville6
 | ligne3_6   |
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
1 row in set (0.00 sec)

 

56.10.1.1.2. La configuration avec les annotations

Le POJO qui encapsule une personne a quelques particularités relatives à la relation avec l'adresse :

Il possède un champ privé de type Adresse

Le champ adresse est annoté avec l'annotation @Embedded

Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "personne")
public class Personne {

  @Id
  @GeneratedValue
  @Column(name = "Id")
  private Long id;

  @Column(name = "Nom")
  private String nom;

  @Column(name = "Prenom")
  private String prenom;

  @Column(name = "DateNais")
  private String dateNais;

  @Embedded
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public Long getId() {
    return id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  //
  // getter et setter sur les autres champs de la classe
  //

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

L'annotation @Embedded permet de préciser que les données de la classe Adresse seront stockées dans la table Personne comme un component d'Hibernate.

Le POJO qui encapsule une adresse possède plusieurs particularités relatives à la relation avec la personne :

  • La classe est annotée avec l'annotation @Embeddable (elle n'est pas annotée comme une entité avec les annotations @Entity et @Table)
  • La classe ne possède pas de champ de type identifiant
Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class Adresse {

  @Column(name = "ligne1_adr", nullable = false)
  private String ligne1;

  @Column(name = "ligne2_adr")
  private String ligne2;

  @Column(name = "cp_adr")
  private String cp;

  @Column(name = "ville_adr")
  private String ville;

  @Column(name = "ligne3_adr")
  private String ligne3;

  public Adresse(String ligne1, String ligne2, String cp, String ville,
      String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }

  public Adresse() {
  }

  //
  // getter et setter sur les champs de la classe
  //
}

L'annotation @Embeddable permet de préciser que la classe sera utilisée comme un component. Un tel élément n'a pas d'identifiant puisque celui utilisé sera celui de l'entité englobante.

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et les deux classes qui encapsulent l'entité Personne et le component Adresse.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property>
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping class="com.jmdoudoux.test.hibernate.Personne"></mapping>
    <mapping class="com.jmdoudoux.test.hibernate.Adresse"></mapping>
  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type Personne,
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;

public class TestHibernate16 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new AnnotationConfiguration().configure()
                                                                 .buildSessionFactory();
    Transaction transaction = null;
    int index = 7;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);

      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi dans la table Personne.

Résultat :
mysql> select * from personne;
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
| Id | Nom  | Prenom   | DateNais | ligne1_adr | ligne2_adr | cp_adr | ville_adr
 | ligne3_adr |
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
|  2 | nom7 | prenom_7 | NULL     | ligne1_7   | ligne2_7   | cp_7   | ville7
 | ligne3_7   |
+----+------+----------+----------+------------+------------+--------+----------
-+------------+
1 row in set (0.00 sec)

 

56.10.1.2. Le mapping avec une relation One-to-One avec clé primaire partagée

La relation repose sur deux tables distinctes : une pour les personnes et une pour les adresses.

Chacune des deux tables possède un identifiant qui est sa clé primaire. La particularité est que la valeur des clés primaires est partagée entre les deux tables. L'identifiant de la table adresse n'est pas auto incrémenté et correspond à la valeur de l'identifiant de la table personne.

Hibernate ne sait pas gérer seul ce type de mapping : il sera nécessaire de l'aider en utilisant un mapping bidirectionnel qui permettra à Hibernate de connaitre la valeur de l'identifiant de la personne à utiliser comme valeur de l'identifiant pour l'adresse afin que les deux correspondent.

La description de la table personne est la suivante :

Résultat :
mysql> desc personne;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| Id       | bigint(20)   |      | PRI | NULL    | auto_increment |
| Nom      | varchar(255) |      |     |         |                |
| Prenom   | varchar(255) |      |     |         |                |
| DateNais | date         | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

Le script DDL correspondant est le suivant :

Résultat :
CREATE TABLE `personne` (
  `Id` bigint(20) NOT NULL auto_increment,
  `Nom` varchar(255) NOT NULL default '',
  `Prenom` varchar(255) NOT NULL default '',
  `DateNais` date default NULL,
  PRIMARY KEY  (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;

La description de la table adresse est la suivante :

Résultat :
mysql> desc adresse;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| id         | bigint(20)  |      | PRI | 0       |       |
| ligne1_adr | varchar(80) |      |     |         |       |
| ligne2_adr | varchar(80) | YES  |     | NULL    |       |
| cp_adr     | varchar(5)  | YES  |     | NULL    |       |
| ville_adr  | varchar(80) | YES  |     | NULL    |       |
| ligne3_adr | varchar(80) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+
6 rows in set (0.11 sec)

Le script DDL correspondant est le suivant :

Résultat :
CREATE TABLE `adresse` (
  `id` bigint(20) NOT NULL default '0',
  `ligne1_adr` varchar(80) NOT NULL default '',
  `ligne2_adr` varchar(80) default NULL,
  `cp_adr` varchar(5) default NULL,
  `ville_adr` varchar(80) default NULL,
  `ligne3_adr` varchar(80) default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

 

56.10.1.2.1. La configuration dans le fichier de mapping

Les classes qui encapsulent les entités personne et adresse sont de simples POJO.

Exemple :
package com.jmdoudoux.test.hibernate;

public class Adresse {

  private Long id;
  private String ligne1;
  private String ligne2;
  private String cp;
  private String ville;
  private String ligne3;

  private Personne personne;

  public Adresse(String ligne1, String ligne2, String cp, String ville,
      String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }

  public Adresse() {
  }

  public Long getId() {
    return id;
  }

  // setter requis par Hibernate
  public void setId(Long id) {
    this.id = id;
  }

  public Personne getPersonne() {
    return personne;
  }

  public void setPersonne(Personne personne) {
    this.personne = personne;
  }

  //
  // getter et setter sur les autres champs de la classe
  //
}

Exemple :
package com.jmdoudoux.test.hibernate;

public class Personne {

  private Long id;
  private String nom;
  private String prenom;
  private String dateNais;
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public Long getId() {
    return id;
  }

  // Attention le setter est requis par Hibernate
  public void setId(Long id) {
    this.id = id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  //
  // getter et setter sur les autres champs de la classe
  //

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

Le fichier de mapping de l'entité Personne (Personne.hbm.xml) contient un élément fils <one-to-one> pour définir la relation entre Personne et Adresse.

Exemple :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.Personne" table="Personne">
    <id name="id" column="id">
      <generator class="increment" />
    </id>
    <property name="nom" column="Nom" />
    <property name="prenom" column="Prenom" />
    <property name="dateNais" column="DateNais" />
    <one-to-one name="adresse" class="com.jmdoudoux.test.hibernate.Adresse" 
      cascade="save-update" />
  </class>
</hibernate-mapping>

Le fichier de mapping de l'entité Adresse (Adresse.hbm.xml) possède plusieurs caractéristiques liées au type de la relation utilisée avec l'entité Personne :

  • Le champ identifiant id est défini avec un générateur de type foreign avec un paramètre qui précise que la valeur sera celle de l'identifiant du champ personne
  • La relation inverse avec Personne est définie avec un tag <one-to-one> avec l'attribut constrained ayant la valeur true
Exemple :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.Adresse" table="Adresse">

    <id name="id" column="Id">
      <generator class="foreign">
        <param name="property">personne</param>
      </generator>
    </id>
    <property name="ligne1" column="ligne1_adr" />
    <property name="ligne2" column="ligne2_adr" />
    <property name="cp" column="cp_adr" />
    <property name="ville" column="ville_adr" />
    <property name="ligne3" column="ligne3_adr" />
    <one-to-one name="personne" class="com.jmdoudoux.test.hibernate.Personne"
      constrained="true" />
  </class>
</hibernate-mapping>

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et les deux fichiers de mapping des entités.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property>   
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping resource="com/jmdoudoux/test/hibernate/Personne.hbm.xml"></mapping>
    <mapping resource="com/jmdoudoux/test/hibernate/Adresse.hbm.xml"></mapping>
  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type Personne,
  • assurer le bon fonctionnement du lien bidirectionnel en fournissant une référence de l'objet Personne à l'instance de l'adresse. Ceci doit être fait manuellement car Hibernate ne prend pas en charge automatiquement les liens bidirectionnels
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class TestHibernate11 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new Configuration().configure()
                                                       .buildSessionFactory();
    Transaction transaction = null;
    int index = 3;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);
      adresse.setPersonne(personne);

      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi avec comme identifiant la valeur de l'identifiant de la personne.

Résultat :
mysql> select * from personne;
+----+------+----------+----------+
| Id | Nom  | Prenom   | DateNais |
+----+------+----------+----------+
|  1 | nom3 | prenom_3 | NULL     |
+----+------+----------+----------+
1 row in set (0.00 sec)

mysql> select * from adresse;
+----+------------+------------+--------+-----------+------------+
| id | ligne1_adr | ligne2_adr | cp_adr | ville_adr | ligne3_adr |
+----+------------+------------+--------+-----------+------------+
|  1 | ligne1_3   | ligne2_3   | cp_3   | ville3    | ligne3_3   |
+----+------------+------------+--------+-----------+------------+
1 row in set (0.00 sec)

 

56.10.1.2.2. La configuration avec les annotations

Le POJO qui encapsule une personne a quelques particularités relatives à la relation avec l'adresse :

  • Il possède un champ privé de type Adresse
  • Le champ adresse est annoté avec les annotations @OneToOne et @PrimaryKeyJoin
Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "personne")
public class Personne {

  @Id
  @GeneratedValue
  @Column(name = "Id")
  private Long id;

  @Column(name = "Nom")
  private String nom;

  @Column(name = "Prenom")
  private String prenom;

  @Column(name = "DateNais")
  private String dateNais;

  @OneToOne(cascade = CascadeType.ALL)
  @PrimaryKeyJoinColumn
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public Long getId() {
    return id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  //
  // getter et setter sur les autres champs de la classe
  //

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

Si le champ adresse n'est pas annoté avec l'annotation @PrimaryKeyJoin, alors une exception de type org.hibernate.id.IdentifierGenerationException avec le message « null id generated for:class com.jmdoudoux.test.hibernate.Adresse » est levée à l'exécution.

Le POJO qui encapsule une adresse possède plusieurs particularités relatives à la relation avec la personne :

  • Le champ identifiant de l'entité est annoté avec @GeneratedValue et @GenericGenerator pour indiquer à Hibernate que la valeur du champ id doit être obtenue à partie de la valeur du champ id de la propriété personne
  • Un champ de type Personne permet une relation bidirectionnelle annotée avec @OneToOne
  • Un setter sur le champ personne permettra d'assurer la cohésion de la relation par le développeur
Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import org.hibernate.annotations.Parameter;

@Entity
@Table(name = "adresse")
public class Adresse {

  @Id
  @GeneratedValue(generator = "adresseGenerator")
  @org.hibernate.annotations.GenericGenerator(name = "adresseGenerator", 
    strategy = "foreign", parameters = @Parameter(name = "property", value = "personne"))
  @Column(name = "id")
  private Long id;

  @Column(name = "ligne1_adr", nullable = false)
  private String ligne1;

  @Column(name = "ligne2_adr")
  private String ligne2;

  @Column(name = "cp_adr")
  private String cp;

  @Column(name = "ville_adr")
  private String ville;

  @Column(name = "ligne3_adr")
  private String ligne3;

  @OneToOne(mappedBy = "adresse")
  private Personne personne;

  public Adresse(String ligne1, String ligne2, String cp, String ville,
      String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }

  public Adresse() {
  }

  public Long getId() {
    return id;
  }

  public Personne getPersonne() {
    return personne;
  }

  public void setPersonne(Personne personne) {
    this.personne = personne;
  }

  //
  // getter et setter sur les autres champs de la classe
  //
}

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et les deux classes qui encapsulent des entités.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property> 
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping class="com.jmdoudoux.test.hibernate.Personne"></mapping>
    <mapping class="com.jmdoudoux.test.hibernate.Adresse"></mapping>
  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type Personne,
  • assurer le bon fonctionnement du lien bidirectionnel en fournissant une référence de l'objet Personne à l'instance de l'adresse. Ceci doit être fait manuellement car Hibernate ne prend pas en charge automatiquement les liens bidirectionnels
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;

public class TestHibernate10 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new AnnotationConfiguration().configure()
                                                                 .buildSessionFactory();
    Transaction transaction = null;
    int index = 2;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);
      adresse.setPersonne(personne);

      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi avec comme identifiant la valeur de l'identifiant de la personne.

Résultat :
mysql> select * from adresse;
+----+------------+------------+--------+-----------+------------+
| id | ligne1_adr | ligne2_adr | cp_adr | ville_adr | ligne3_adr |
+----+------------+------------+--------+-----------+------------+
|  3 | ligne1_1   | ligne2_1   | cp_1   | ville1    | ligne3_1   |
+----+------------+------------+--------+-----------+------------+
1 row in set (0.00 sec)

mysql> select * from personne;
+----+------+----------+----------+
| Id | Nom  | Prenom   | DateNais |
+----+------+----------+----------+
|  3 | nom1 | prenom_1 | NULL     |
+----+------+----------+----------+
1 row in set (0.00 sec)

Si la référence de l'instance de type Personne n'est pas fournie à l'instance de type Adresse alors une exception de type org.hibernate.id.IdentifierGenerationException avec le message « attempted to assign id from null one-to-one property [com.jmdoudoux.test.hibernate.Adresse.personne] » est levée à l'exécution.

Si le générateur d'identifiant n'est pas correctement configuré pour l'entité Adresse, alors une exception de type org.hibernate.id.IdentifierGenerationException avec le message « ids for this class must be manually assigned before calling save(): com.jmdoudoux.test.hibernate.Adresse » lors de l'exécution.

 

56.10.1.3. Le mapping avec une relation One-to-One avec clé étrangère

La relation repose sur deux tables distinctes : une pour les personnes et une pour les adresses

Chacune des deux tables possède son propre identifiant et la relation entre les deux tables est assurée par une clé étrangère de la table personne vers la table adresse.

Hibernate sait gérer seul ce type de mapping s'il est unidirectionnel.

La description de la table personne est la suivante :

Résultat :
mysql> desc personne;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| Id         | int(11)      |      | PRI | NULL    | auto_increment |
| Nom        | varchar(255) |      |     |         |                |
| Prenom     | varchar(255) |      |     |         |                |
| DateNais   | date         | YES  |     | NULL    |                |
| adresse_id | int(11)      |      |     | 0       |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Le script DDL correspondant est le suivant :

Résultat :
CREATE TABLE `personne` (
  `Id` int(11) NOT NULL auto_increment,
  `Nom` varchar(255) NOT NULL default '',
  `Prenom` varchar(255) NOT NULL default '',
  `DateNais` date default NULL,
  `adresse_id` int(11) NOT NULL default '0',
  PRIMARY KEY  (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;

La description de la table adresse est la suivante :

Résultat :
mysql> desc adresse;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | bigint(20)  |      | PRI | NULL    | auto_increment |
| ligne1_adr | varchar(80) |      |     |         |                |
| ligne2_adr | varchar(80) | YES  |     | NULL    |                |
| cp_adr     | varchar(5)  | YES  |     | NULL    |                |
| ville_adr  | varchar(80) | YES  |     | NULL    |                |
| ligne3_adr | varchar(80) | YES  |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

Le script DDL correspondant est le suivant :

Résultat :
CREATE TABLE `adresse` (
  `id` bigint(20) NOT NULL auto_increment,
  `ligne1_adr` varchar(80) NOT NULL default '',
  `ligne2_adr` varchar(80) default NULL,
  `cp_adr` varchar(5) default NULL,
  `ville_adr` varchar(80) default NULL,
  `ligne3_adr` varchar(80) default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;

 

56.10.1.3.1. La configuration dans le fichier de mapping

Les classes qui encapsulent les entités personne et adresse sont de simples POJO.

Exemple :
package com.jmdoudoux.test.hibernate;

public class Personne {

  private Long id;
  private String nom;
  private String prenom;
  private String dateNais;
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public Long getId() {
    return id;
  }

  // Attention le setter est requis par Hibernate
  public void setId(Long id) {
    this.id = id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  //
  // getter et setter sur les autres champs de la classe
  //

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

Exemple :
package com.jmdoudoux.test.hibernate;
public class Adresse {
  private Long id;
  private String ligne1;
  private String ligne2;
  private String cp;
  private String ville;
  private String ligne3;

  public Adresse(String ligne1, String ligne2, String cp, String ville, String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }
  
  public Adresse() {
  }
  
  public Long getId() {
    return id;
  }
  
  // setter requis par Hibernate
  public void setId(Long id) {
    this.id = id;
  }

  //
  // getter et setter sur les autres champs de la classe
  //
}

Le fichier de mapping de l'entité Personne (Personne.hbm.xml) contient un élément fils <many-to-one> pour définir la relation entre Personne et Adresse : l'unicité de la relation est cependant garantie par la valeur true de l'attribut unique. Il faut aussi utiliser une propriété column pour préciser la colonne qui va contenir la clé étrangère vers la table adresse.

Exemple :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.Personne" table="Personne">
    <id name="id" column="Id">
       <generator class="increment" />
    </id>
    <property name="nom" column="Nom" />
    <property name="prenom" column="Prenom" />
    <property name="dateNais" column="DateNais" />
    <many-to-one name="adresse" class="com.jmdoudoux.test.hibernate.Adresse"
        column="adresse_id" cascade="all" unique="true" />
  </class>
</hibernate-mapping>

Le fichier de mapping de l'entité Adresse (Adresse.hbm.xml) ne contient aucune particularité.

Exemple :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.Adresse" table="Adresse">
    <id name="id">
      <generator class="increment" />
    </id>
    <property name="ligne1" column="ligne1_adr" />
    <property name="ligne2" column="ligne2_adr" />
    <property name="cp" column="cp_adr" />
    <property name="ville" column="ville_adr" />
    <property name="ligne3" column="ligne3_adr" />
  </class>
</hibernate-mapping>

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et les deux fichiers de mapping des entités.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property>
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping resource="com/jmdoudoux/test/hibernate/Personne.hbm.xml"></mapping>
    <mapping resource="com/jmdoudoux/test/hibernate/Adresse.hbm.xml"></mapping>
  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type Personne,
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class TestHibernate12 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new Configuration().configure()
                                                       .buildSessionFactory();
    Transaction transaction = null;
    int index = 4;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom_" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);

      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi. Les nouvelles occurrences des tables Personne et Adresse possèdent chacun leur propre identifiant et celui de l'adresse est reporté dans le champ adresse_id de la table Personne.

Résultat :
mysql> select * from personne;
+----+-------+----------+----------+------------+
| Id | Nom   | Prenom   | DateNais | adresse_id |
+----+-------+----------+----------+------------+
|  1 | nom_4 | prenom_4 | NULL     |          8 |
+----+-------+----------+----------+------------+
1 row in set (0.82 sec)

mysql> select * from adresse;
+----+------------+------------+--------+-----------+------------+
| id | ligne1_adr | ligne2_adr | cp_adr | ville_adr | ligne3_adr |
+----+------------+------------+--------+-----------+------------+
|  8 | ligne1_4   | ligne2_4   | cp_4   | ville4    | ligne3_4   |
+----+------------+------------+--------+-----------+------------+
1 row in set (0.00 sec)

 

56.10.1.3.2. La configuration avec les annotations

Le POJO qui encapsule une personne a quelques particularités relatives à la relation avec l'adresse :

  • Il possède un champ privé de type Adresse
  • Le champ adresse est annoté avec les annotations @OneToOne et @JoinColumn
Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

@Entity
@Table(name = "personne")
public class Personne {

  @Id
  @GeneratedValue
  @Column(name = "Id")
  private Long id;

  @Column(name = "Nom")
  private String nom;

  @Column(name = "Prenom")
  private String prenom;

  @Column(name = "DateNais")
  private String dateNais;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "adresse_id")
  private Adresse adresse;

  public Personne(String nom, String prenom, String dateNais, Adresse adresse) {
    this.nom = nom;
    this.prenom = prenom;
    this.dateNais = dateNais;
    this.adresse = adresse;
  }

  public Personne() {
  }

  public Long getId() {
    return id;
  }

  public Adresse getAdresse() {
    return adresse;
  }

  public void setAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  //
  // getter et setter sur les autres champs de la classe
  //

  @Override
  public String toString() {
    return this.id + " : " + this.nom + " " + this.prenom;
  }
}

Le POJO qui encapsule une adresse ne possède aucune particularité relative à la relation avec la personne. Il est cependant possible d'ajouter au besoin une relation inverse d'adresse vers personne en ajoutant un champ Personne annoté avec l'annotation @one-to-one possèdant un attribut mappedBy qui possède comme valeur le nom du champ de l'adresse dans l'entité Personne.

Dans ce cas, la gestion de l'alimentation du champ personne est à la charge du développeur en utilisant le setter sur le champ personne.

L'identifiant de l'entité est annoté avec @GeneratedValue

Exemple :
package com.jmdoudoux.test.hibernate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "adresse")
public class Adresse {

  @Id
  @GeneratedValue
  @Column(name = "id")
  private Long id;

  @Column(name = "ligne1_adr", nullable = false)
  private String ligne1;

  @Column(name = "ligne2_adr")
  private String ligne2;

  @Column(name = "cp_adr")
  private String cp;

  @Column(name = "ville_adr")
  private String ville;

  @Column(name = "ligne3_adr")
  private String ligne3;

  public Adresse(String ligne1, String ligne2, String cp, String ville,
      String ligne3) {
    super();
    this.ligne1 = ligne1;
    this.ligne2 = ligne2;
    this.cp = cp;
    this.ville = ville;
    this.ligne3 = ligne3;
  }

  public Adresse() {
  }

  public Long getId() {
    return id;
  }

  //
  // getter et setter sur les autres champs de la classe
  //
}

Le fichier de configuration d'Hibernate définit les paramètres de connexion à la base de données et les deux classes qui encapsulent des entités.

Exemple :
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:mysql://localhost/mabdd</property>
    <property name="connection.username">root</property>
    <property name="connection.password"></property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory</property>   
    <property name="current_session_context_class">thread</property>
    <property name="hibernate.show_sql">true</property>
    <mapping class="com.jmdoudoux.test.hibernate.Personne"></mapping>
    <mapping class="com.jmdoudoux.test.hibernate.Adresse"></mapping>

  </session-factory>
</hibernate-configuration>

L'application de test est basique :

  • créer une instance de type Adresse,
  • créer une instance de type Personne,
  • et sauvegarder la personne dans la base de données
Exemple :
package com.jmdoudoux.test.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;

public class TestHibernate14 {

  public static void main(String args[]) {
    SessionFactory sessionFactory = new AnnotationConfiguration().configure()
                                                                 .buildSessionFactory();
    Transaction transaction = null;
    int index = 5;

    Session session = sessionFactory.openSession();

    try {
      transaction = session.beginTransaction();

      Adresse adresse = new Adresse("ligne1_" + index, "ligne2_" + index, "cp_"
          + index, "ville" + index, "ligne3_" + index);
      Personne personne = new Personne("nom" + index,
                                       "prenom_" + index,
                                       null,
                                       adresse);
      session.save(personne);
      transaction.commit();

      System.out.println("La nouvelle personne a ete enregistree");

    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    sessionFactory.close();
  }
}

Lorsque la personne est enregistrée dans la base de données, son adresse l'est aussi. Chaque nouvelle occurrence de la table Personne et de la table Adresse possède son propre identifiant et celui de l'adresse est reporté dans le champ adresse_id de la table Personne.

Résultat :
mysql> select * from adresse;
+----+------------+------------+--------+-----------+------------+
| id | ligne1_adr | ligne2_adr | cp_adr | ville_adr | ligne3_adr |
+----+------------+------------+--------+-----------+------------+
| 10 | ligne1_5   | ligne2_5   | cp_5   | ville5    | ligne3_5   |
+----+------------+------------+--------+-----------+------------+
1 row in set (0.00 sec)

mysql> select * from personne;
+----+-------+----------+----------+------------+
| Id | Nom   | Prenom   | DateNais | adresse_id |
+----+-------+----------+----------+------------+
|  8 | nom5  | prenom_5 | NULL     |         10 |
+----+-------+----------+----------+------------+
1 row in set (0.00 sec)

 

en construction
La suite de cette section sera développée dans une version future de ce document

 

56.11. Le mapping de l'héritage de classes

Hibernate propose un support des trois stratégies de base pour le mapping d'héritage de classes :

  • une table par hiérarchie de classes (Table Per Hierarchy) : une seule table est utilisée. Elle possède une colonne supplémentaire qui sert de discriminant en précisant le type des données de la ligne
  • une table par classe concrète (Table Per Concrete class) : à chaque classe concrète correspond une table. Les champs communs sont dupliqués dans chaque table fille
  • une table par sous-classe (Table Per Subclass) : à chaque classe correspond une table. Les relations entre ces tables se font en utilisant des relations par clés étrangères. Il n'y a donc pas de colonnes dupliquées. Chaque classe est mappée à sa propre table.

Hibernate propose aussi une autre stratégie nommée une table par sous-classe avec discriminant.

Pour mapper une relation d'héritage dans le modèle relationnel, il faut donc choisir une stratégie qui sera adaptée en fonction des besoins.

Les exemples de cette section vont utiliser une hiérarchie de classes composée d'une classe mère Compte et deux classes filles CompteEpargne et CompteCourant.

La même classe est utilisée pour mettre en oeuvre des traitements avec les différentes stratégies de mapping.

Exemple :
package com.jmdoudoux.test.hibernate;
      
import java.math.BigDecimal;
import java.util.List; 
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import com.jmdoudoux.test.hibernate.entity.Compte;
import com.jmdoudoux.test.hibernate.entity.CompteCourant;
import com.jmdoudoux.test.hibernate.entity.CompteEpargne;

public class TestHibernate {

  private static SessionFactory sessionFactory = null;

  public static void main(String[] args) {
    try {
      Configuration hibernateConfig = new Configuration().configure();
      ServiceRegistry serviceRegistry = new ServiceRegistryBuilder()
          .applySettings(hibernateConfig.getProperties())
          .buildServiceRegistry();
      sessionFactory = hibernateConfig.buildSessionFactory(serviceRegistry);
      creerComptes();
      rechercherChaqueCompte();
      rechercherTousLesComptes();
      rechercheComptesPolymorphiques();
    } catch (Throwable ex) {
      ex.printStackTrace();
    } finally {
      sessionFactory.close();
    }
  }
  
  public static Transaction creerComptes() {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      Compte compte = new Compte();
      compte.setNumero("000012345000");
      compte.setSolde(BigDecimal.ZERO);
      CompteCourant compteCourant = new CompteCourant();
      compteCourant.setNumero("000012345010");
      compteCourant.setSolde(new BigDecimal("1200"));
      compteCourant.setDecouvert(2000);
      CompteEpargne compteEpargne = new CompteEpargne();
      compteEpargne.setNumero("000012345020");
      compteEpargne.setSolde(new BigDecimal(8000));
      compteEpargne.setTaux(new BigDecimal("2.10"));
      
      tx = session.beginTransaction();
      session.save(compte);
      session.save(compteCourant);
      session.save(compteEpargne);
      tx.commit();
    } catch (RuntimeException e) {
      try {
        tx.rollback();
      } catch (RuntimeException rbe) {
        rbe.printStackTrace();
      }
      throw e;
    } finally {
      if (session != null)
        session.close();
    }
    return tx;
  }
  
  public static void rechercherChaqueCompte() {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      System.out.println("Recherche d'un compte");
      Compte compte = (Compte) session.load(Compte.class, new Integer(1));
      System.out.println(compte);
      System.out.println();
      System.out.println("Recherche d'un compte courant");
      CompteCourant compteCourant = (CompteCourant) session.load(
          CompteCourant.class, new Integer(2));
      System.out.println(compteCourant);
      System.out.println();
      System.out.println("Recherche polymorphique d'un compte");
      compte = (Compte) session.load(Compte.class, new Integer(3));
      System.out.println(compte);
      System.out.println();
      tx.commit();
    } catch (RuntimeException e) {
      try {
        tx.rollback();
      } catch (RuntimeException rbe) {
        rbe.printStackTrace();
      }
      throw e;
    } finally {
      if (session != null)
        session.close();
    }
  }
  
  public static void rechercherTousLesComptes() {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    System.out.println("Recherche de tous les comptes");
    try {
      tx = session.beginTransaction();
      Query query = session.createQuery("from Compte");
      List<Compte> comptes = query.list();
      for (Compte compte : comptes) {
        System.out.println(compte);
      }
      System.out.println();
      tx.commit();
    } catch (RuntimeException e) {
      try {
        tx.rollback();
      } catch (RuntimeException rbe) {
        rbe.printStackTrace();
      }
      throw e;
    } finally {
      if (session != null)
        session.close();
    }
  }
  
  public static void rechercheComptesPolymorphiques() {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    System.out.println("Recherche polymorphique de comptes");
    try {
      tx = session.beginTransaction();
      Query query = session
          .createQuery("select c from Compte c where c.numero like :numero");
      query.setParameter("numero", "000012345%");
      List<Compte> comptes = query.list();
      for (Compte compte : comptes) {
        System.out.println(compte);
      }
      tx.commit();
    } catch (RuntimeException e) {
      try {
        tx.rollback();
      } catch (RuntimeException rbe) {
        rbe.printStackTrace();
      }
      throw e;
    } finally {
      if (session != null)
        session.close();
    }
    System.out.println();
  }
}

La version d'Hibernate utilisée est la 4.2.17.

 

56.11.1. XML

La déclaration du mapping dans un fichier XML utilise plusieurs tags en fonction de la stratégie utilisée : <class>, <union-subclass>, <subclass> et <joined-subclass>.

Il n'est pas possible de mixer l'utilisation du tag <subclass> et <joined-subclass> dans le même tag <class>.

Les exemples des sections suivantes utilisent trois classes qui sont les entités du modèle.

Exemple :
package com.jmdoudoux.test.hibernate.entity;

import java.math.BigDecimal;

public class Compte {
  protected int        id;
  protected String     numero;
  protected BigDecimal solde;
  
  public int getId() {
    return id;
  }
  
  public void setId(int id) {
    this.id = id;
  }
  
  public String getNumero() {
    return numero;
  }
  
  public void setNumero(String numero) {
    this.numero = numero;
  }
  
  public BigDecimal getSolde() {
    return solde;
  }
  
  public void setSolde(BigDecimal solde) {
    this.solde = solde;
  }
  
  @Override
  public String toString() {
    return super.toString() + " [id=" + id + ", numero=" + numero + ", solde="
        + solde + "]";
  }
} 

Exemple :
package com.jmdoudoux.test.hibernate.entity;
      
public class CompteCourant extends Compte {
  protected int decouvert;
  
  public int getDecouvert() {
    return decouvert;
  }
  
  public void setDecouvert(int decouvert) {
    this.decouvert = decouvert;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteCourant [id=" + id + ", numero=" + numero + ", solde=" + solde
        + ", decouvert=" + decouvert + "]";
  }
}

Exemple :
package com.jmdoudoux.test.hibernate.entity;
      
import java.math.BigDecimal;

public class CompteEpargne extends Compte {
  protected BigDecimal taux;
  
  public BigDecimal getTaux() {
    return taux;
  }
  
  public void setTaux(BigDecimal taux) {
    this.taux = taux;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteEpargne [ id=" + id + ", numero=" + numero + ", solde="
        + solde + ", taux=" + taux + "]";
  }
}

La configuration d'Hibernate est stockée dans le fichier hibernate.cfg.xml.

Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3307/mabdd</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">root</property>
    <property name="hibernate.connection.pool_size">1</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Cache de second niveau désactivé  -->
    <property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property>
    <!-- Afficher les requêtes SQL exécutées sur la sortie standard -->
    <property name="hibernate.show_sql">true</property>
    
    <property name="hbm2ddl.auto">create</property> 
    
    <mapping resource="Compte.hbm.xml"/>
  </session-factory>
</hibernate-configuration>

Remarque : La propriété hbm2dll.auto est initialisée avec la valeur create pour demander à Hibernate de recréer automatiquement les tables à chaque exécution.

 

56.11.1.1. XML, une table par hiérarchie de classes

Pour définir le mapping d'un héritage utilisant le modèle une table par hiérarchie de classe, il faut utiliser dans un même fichier hbm :

  • un tag <class> pour le mapping classe mère
  • un tag <subclass> pour le mapping de chaque classe fille

Le tag <class> possède l'attribut polymorphism qui peut prendre deux valeurs (implicit ou explicit) et permet de préciser le type de requête polymorphique.

L'attribut abstract permet de préciser si la classe mère de la hiérarchie est abstraite. Les valeurs possibles sont true et false

Le tag <discriminator> permet de préciser la colonne qui servira de discriminant.

Il possède plusieurs attributs :

column 

Préciser le nom de la colonne dans la table, par défaut c'est le nom de la classe

formula 

Une expression SQL qui sera exécutée pour obtenir la valeur. Optionnel

type 

Préciser le type de la donnée. Optionnel : par défaut, String

not-null 

true (par défaut) ou false

length 

Préciser la taille de la colonne

force 

true ou false. Optionnel : par défaut, false

insert 

Préciser si la colonne doit être incluse dans les instructions insert : true ou false. Optionnel : par défaut, true


Le tag <subclass> possède plusieurs attributs :

entity-name 

Optionnel

name 

Nom pleinement qualifié de la classe de l'entité

proxy 

Interface ou classe qui est utilisée pour la création des proxys. Optionnel

discriminator-value

Une valeur qui permet de distinguer chaque entité. Optionnel : par défaut c'est le nom de la classe

dynamic-update

true ou false (par défaut)

dynamic-insert

true ou false (par défaut)

select-before-update

true ou false (par défaut)

extends

Nom de la superclasse

lazy

Activation du lazy fetching : true ou false. Optionnel : par défaut true

abstract

true ou false

persister 

Préciser un ClassPersister personnalisé. Optionnel

batch-size 

Définir le nombre d'occurrences récupérées par lot. Optionnel

node 

 

Les tags <class> et <subclass> possèdent un attribut discriminator-value qui permet de préciser la valeur de la colonne discriminante qui sera utilisée pour la classe. Il n'est pas nécessaire de fournir une valeur à l'attribut discriminator-value pour les classes abstraites.

La colonne discriminator ne doit pas être définie dans la classe Java correspondante : c'est une colonne technique qui n'est utilisée que par Hibernate et la base de données.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
 "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.entity.Compte"
    table="compte" discriminator-value="Compte">
    <id name="id" column="id" type="int" >
      <generator class="native" />
    </id>
    <discriminator column="DTYPE" type="string" />
        
    <property name="numero" column="numero" type="string" />
    <property name="solde" column="solde" type="big_decimal" />
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteCourant"
      discriminator-value="CompteCourant">
      <property name="decouvert" column="decouvert" type="int"/>
    </subclass>
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteEpargne"
      discriminator-value="CompteEpargne">
      <property name="taux" column="taux" type="big_decimal"/>
    </subclass>      
  </class>
</hibernate-mapping>

Lorsque le type d'entité demandé à Hibernate est précisément identifié, il peut faire une requête qui ne récupère que les colonnes nécessaires à l'alimentation des propriétés de la classe.

Lorsque le type d'entité demandé à Hibernate est un supertype, celui-ci ne peut pas déterminer à l'avance les données nécessaires donc il doit obtenir toutes les données. A partir du discriminant, Hibernate peut créer la bonne instance et alimenter ses propriétés.

Résultat :
2015-02-23 23:18:53.650 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-02-23 23:18:53.663 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-02-23 23:18:53.665 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-02-23 23:18:53.666 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-02-23 23:18:53.700 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-02-23 23:18:53.700 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-02-23 23:18:53.753 INFO   [main]:org.hibernate.cfg.Configuration - HHH000221: Reading 
mappings from resource: Compte.hbm.xml
2015-02-23 23:18:53.823 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-02-23 23:18:53.926 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-02-23 23:18:53.941 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-02-23 23:18:53.941 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-02-23 23:18:53.942 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-02-23 23:18:53.942 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-02-23 23:18:54.340 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-02-23 23:18:54.361 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-02-23 23:18:54.366 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-02-23 23:18:54.617 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: drop table if exists compte
Hibernate: create table compte (id integer not null auto_increment, DTYPE varchar(255) not 
null, numero varchar(255), solde decimal(19,2), decouvert integer, taux decimal(19,2), primary 
key (id))
2015-02-23 23:18:54.707 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: insert into compte (numero, solde, DTYPE) values (?, ?, 'Compte')
Hibernate: insert into compte (numero, solde, decouvert, DTYPE) values (?, ?, ?, '
CompteCourant')
Hibernate: insert into compte (numero, solde, taux, DTYPE) values (?, ?, ?, 'CompteEpargne')
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert0_0_, compte0_.taux as taux0_0_, compte0_.DTYPE as 
DTYPE0_0_ from compte compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@3c3daf [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_.decouvert as decouvert0_0_ from compte comptecour0_ where 
comptecour0_.id=? and comptecour0_.DTYPE='CompteCourant'
com.jmdoudoux.test.hibernate.entity.CompteCourant@26380823CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert0_0_, compte0_.taux as taux0_0_, compte0_.DTYPE as 
DTYPE0_0_ from compte compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@641293CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert0_, compte0_.taux as taux0_, compte0_.DTYPE as DTYPE0_ from 
compte compte0_
com.jmdoudoux.test.hibernate.entity.Compte@1c458a5 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@953079CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@12941261CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert0_, compte0_.taux as taux0_, compte0_.DTYPE as DTYPE0_ from 
compte compte0_ where compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.Compte@4cb73c [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@9557173CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@975425CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Résultat :
mysql> select * from compte;
+----+---------------+--------------+---------+-----------+------+
| id | DTYPE         | numero       | solde   | decouvert | taux |
+----+---------------+--------------+---------+-----------+------+
|  1 | Compte        | 000012345000 |    0.00 |      NULL | NULL |
|  2 | CompteCourant | 000012345010 | 1200.00 |      2000 | NULL |
|  3 | CompteEpargne | 000012345020 | 8000.00 |      NULL | 2.10 |
+----+---------------+--------------+---------+-----------+------+
3 rows in set (0.00 sec)

 

56.11.1.2. XML, une table par sous-classe

Le modèle relationnel et le modèle objet correspondant sont très proches : une table est utilisée pour chaque classe de la hiérarchie.

Les données d'une classe fille sont réparties dans la table de la classe mère pour les données héritées et dans la table de la classe fille pour ses données spécifiques. Cette stratégie ne requière pas de colonne de type discriminant.

La définition du mapping de la classe mère et de la classe fille se fait dans le même fichier .hbm. Pour définir le mapping d'un héritage utilisant le modèle une table par sous-classe, il faut utiliser dans un même fichier hbm :

  • un tag <class> pour le mapping classe mère
  • un tag <joined-subclass> pour le mapping de chaque classe fille

Le tag <joined-subclass> possède plusieurs attributs :

Nom

Rôle

entity-name

Par défaut, le nom de la classe

name 

Nom pleinement qualifié de la classe de l'entité

proxy 

Nom de la classe ou de l'interface utilisée pour créer des proxys lors des lectures lazy. Optionnel

table 

Nom de la table. par défaut le nom de la classe

schema

Nom du schéma qui contiendra la table

catalog 

Nom du catalogue qui contiendra la table

subselect 

Requête SQL qui sera exécutée pour obtenir les données immuables de l'entité

dynamic-update 

true ou false (par défaut)

dynamic-insert 

true ou false (par défaut)

select-before-update 

true ou false (par défaut)

extends

Nom de la superclasse pour une entité fille. Optionnel

lazy 

Activation du lazy fetching : true ou false. Optionnel : par défaut true

abstract

true ou false : préciser si la classe est abstraite

persister

Nom de la classe de type ClassPersister à utiliser

check

Optionnel

batch-size

Préciser le nombre d'occurrences obtenues lors de la lecture par lots. Optionnel

node

 

Lors de l'utilisation de cette stratégie, la superclasse a une table et chaque sous-classe a une table qui contient seulement ses propriétés non-héritées : les tables des classes filles ont une clé primaire est une clé étrangère vers la table de la superclasse. Hibernate va utiliser une relation de type 1-1 entre la clé primaire de la table mère et la clé étrangère de la table fille. Ceci implique une jointure entre les deux tables pour obtenir les données.

Le tag <id> du tag <class> permet de préciser la colonne qui est la clé primaire.

Le tag <key> fils du tag <joined-subclass> permet de préciser la clé étrangère qui sera utilisée pour réaliser la jointure avec la table mère sur sa clé primaire.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
 "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.jmdoudoux.test.hibernate.entity.Compte" table="compte" >
        <id name="id" column="id" type="int" >
            <generator class="native" />
        </id>
        <property name="numero" column="numero" type="string" />
        <property name="solde" column="solde" type="big_decimal" />
        <joined-subclass name="com.jmdoudoux.test.hibernate.entity.CompteCourant" 
            table="compte_courant">
            <key column="id"/>
            <property name="decouvert" column="decouvert" type="int"/>
        </joined-subclass>
        <joined-subclass name="com.jmdoudoux.test.hibernate.entity.CompteEpargne"
            table="compte_epargne">
            <key column="id"/>
            <property name="taux" column="taux" type="big_decimal"/>
        </joined-subclass>      
    </class>
</hibernate-mapping>

Pour obtenir une occurrence de l'entité, Hibernate va automatiquement effectuer une jointure entre la table mère et la table de la classe correspondante.

Si la requête demandée à Hibernate concerne le type d'une classe abstraite, Hibernate récupère de la base de données toutes les données des occurrences relatives à toutes les classes concrètes en effectuant une jointure sur toutes les tables. Hibernate est obligé de faire une jointure sur toutes les tables : il crée alors une instance de l'entité. Cependant, dans ce cas toutes les autres colonnes des autres tables sont lues.

Si la requête demandée à Hibernate concerne le type d'une classe concrète, Hibernate effectue une jointure entre la table de la classe mère et la table de la classe fille correspondante.

Lors de l'enregistrement d'une nouvelle classe dans la base de données, Hibernate sa créer une nouvelle occurrence dans la table de la classe mère puis dans la table de la classe fille. La clé de la classe mère sera utilisée comme valeur dans la clé étrangère de la classe fille pour permettre à Hibernate de faire la jointure lors de la lecture des données.

Résultat :
2015-02-11 22:34:44.154 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-02-11 22:34:44.172 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-02-11 22:34:44.174 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-02-11 22:34:44.175 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-02-11 22:34:44.211 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-02-11 22:34:44.211 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-02-11 22:34:44.297 INFO   [main]:org.hibernate.cfg.Configuration - HHH000221: Reading 
mappings from resource: Compte.hbm.xml
2015-02-11 22:34:44.366 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-02-11 22:34:44.475 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-02-11 22:34:44.482 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-02-11 22:34:44.483 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-02-11 22:34:44.483 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-02-11 22:34:44.485 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-02-11 22:34:44.842 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-02-11 22:34:44.870 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-02-11 22:34:44.874 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-02-11 22:34:45.100 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: alter table compte_courant drop foreign key FK2435FDBF9CCD1BB4
2015-02-11 22:34:45.106 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000389: 
Unsuccessful: alter table compte_courant drop foreign key FK2435FDBF9CCD1BB4
2015-02-11 22:34:45.106 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - Table 'mabdd.
compte_courant' doesn't exist
Hibernate: alter table compte_epargne drop foreign key FK8E9D8D439CCD1BB4
2015-02-11 22:34:45.107 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000389: 
Unsuccessful: alter table compte_epargne drop foreign key FK8E9D8D439CCD1BB4
2015-02-11 22:34:45.107 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - Table 'mabdd.
compte_epargne' doesn't exist
Hibernate: drop table if exists compte
Hibernate: drop table if exists compte_courant
Hibernate: drop table if exists compte_epargne
Hibernate: create table compte (id integer not null auto_increment, numero varchar(255), solde 
decimal(19,2), primary key (id))
Hibernate: create table compte_courant (id integer not null, decouvert integer, primary key (
id))
Hibernate: create table compte_epargne (id integer not null, taux decimal(19,2), primary key (
id))
Hibernate: alter table compte_courant add index FK2435FDBF9CCD1BB4 (id), add constraint 
FK2435FDBF9CCD1BB4 foreign key (id) references compte (id)
Hibernate: alter table compte_epargne add index FK8E9D8D439CCD1BB4 (id), add constraint 
FK8E9D8D439CCD1BB4 foreign key (id) references compte (id)
2015-02-11 22:34:45.189 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte_courant (decouvert, id) values (?, ?)
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte_epargne (taux, id) values (?, ?)
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, case when 
compte0_1_.id is not null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not 
null then 0 end as clazz_0_ from compte compte0_ left outer join compte_courant compte0_1_ on 
compte0_.id=compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.
id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@e2ecb3 [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_1_.numero as numero0_0_, 
comptecour0_1_.solde as solde0_0_, comptecour0_.decouvert as decouvert1_0_ from compte_courant 
comptecour0_ inner join compte comptecour0_1_ on comptecour0_.id=comptecour0_1_.id where 
comptecour0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteCourant@1972965CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, case when 
compte0_1_.id is not null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not 
null then 0 end as clazz_0_ from compte compte0_ left outer join compte_courant compte0_1_ on 
compte0_.id=compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.
id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@32800355CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_1_.decouvert as decouvert1_, compte0_2_.taux as taux2_, case when compte0_1_.id is not 
null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not null then 0 end as 
clazz_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.id
com.jmdoudoux.test.hibernate.entity.CompteCourant@2473102CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@21374993CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
com.jmdoudoux.test.hibernate.entity.Compte@f08a49 [id=1, numero=000012345000, solde=0.00]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_1_.decouvert as decouvert1_, compte0_2_.taux as taux2_, case when compte0_1_.id is not 
null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not null then 0 end as 
clazz_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.id where 
compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.CompteCourant@5917204CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@28671960CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
com.jmdoudoux.test.hibernate.entity.Compte@ad3eb5 [id=1, numero=000012345000, solde=0.00]

Résultat :
mysql> select * from compte;
+----+--------------+---------+
| id | numero       | solde   |
+----+--------------+---------+
|  1 | 000012345000 |    0.00 |
|  2 | 000012345010 | 1200.00 |
|  3 | 000012345020 | 8000.00 |
+----+--------------+---------+
3 rows in set (0.00 sec)
mysql> select * from compte_courant;
+----+-----------+
| id | decouvert |
+----+-----------+
|  2 |      2000 |
+----+-----------+
1 row in set (0.00 sec)
mysql> select * from compte_epargne;
+----+------+
| id | taux |
+----+------+
|  3 | 2.10 |
+----+------+
1 row in set (0.00 sec)

Remarque : l'identifiant de l'occurrence de la table de la classe mère et celui de l'occurrence de la table de la classe fille doivent être identiques pour permettre de réaliser la jointure.

 

56.11.1.3. XML, une table par classe concrète

Dans cette stratégie, chaque classe concrète est mappée sur une table dans la base de données. Toutes les propriétés de la classe sont partagées par les classes filles : ceci implique une duplication de ces champs dans les tables des classes filles.

La classe mère est définie grâce à un tag <class>.

Chaque classe fille est définie grâce à un tag fils <union-subclass>. Il possède plusieurs attributs :

  • name : nom pleinement qualifié de la classe
  • table : nom de la table dans la base de données

Les colonnes de la table de la classe mère sont aussi présentes dans les tables des classes filles. Chaque propriété propre à une classe fille doit être mappée dans le tag <union-subclass> correspondant.

Le tag <union-subclass> possède plusieurs attributs :

Attribut

Rôle

entity-name

Par défaut, le nom de la classe

name

Nom pleinement qualifié de la classe

proxy

Interface ou classe qui est utilisée pour la création des proxys. Optionnel

table

Nom de la table. Par défaut : le nom de la classe

schema 

Nom du schéma qui contiendra la table

catalog 

Nom du catalogue qui contiendra la table

subselect 

Requête SQL qui sera exécutée pour obtenir les données immuables de l'entité

dynamic-update 

true ou false (par défaut)

dynamic-insert 

true ou false (par défaut)

select-before-update 

true ou false (par défaut)

extends

Nom de la super classe pour une entité fille. Optionnel

lazy 

true ou false

abstract 

true ou false

persister

Préciser un ClassPersister personnalisé. Optionnel

check

Optionnel

batch-size

Définir le nombre d'occurrences récupérées par lot. Optionnel

node

 

Le champ identifiant de la classe mère est partagé avec les classes filles : ce champ n'est d'ailleurs pas mappé dans les classes filles.

Remarque : il n'est pas possible d'utiliser la stratégie de gestion native des identifiants.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
 "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.entity.Compte"
    table="compte" >
    <id name="id" column="id" type="int" >
      <generator class="increment" />
    </id>
    <property name="numero" column="numero" type="string" />
    <property name="solde" column="solde" type="big_decimal" />
    <union-subclass name="com.jmdoudoux.test.hibernate.entity.CompteCourant"
      table="compte_courant">
      <property name="decouvert" column="decouvert" type="int"/>
    </union-subclass>
    <union-subclass name="com.jmdoudoux.test.hibernate.entity.CompteEpargne"
      table="compte_epargne">
      <property name="taux" column="taux" type="big_decimal"/>
    </union-subclass>      
  </class>
</hibernate-mapping>

Résultat :
2015-02-11 22:37:45.178 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-02-11 22:37:45.194 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-02-11 22:37:45.194 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-02-11 22:37:45.194 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-02-11 22:37:45.225 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-02-11 22:37:45.225 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-02-11 22:37:45.272 INFO   [main]:org.hibernate.cfg.Configuration - HHH000221: Reading 
mappings from resource: Compte.hbm.xml
2015-02-11 22:37:45.319 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-02-11 22:37:45.443 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-02-11 22:37:45.443 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-02-11 22:37:45.443 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-02-11 22:37:45.443 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-02-11 22:37:45.459 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-02-11 22:37:45.787 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-02-11 22:37:45.803 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-02-11 22:37:45.803 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-02-11 22:37:46.014 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: drop table if exists compte
Hibernate: drop table if exists compte_courant
Hibernate: drop table if exists compte_epargne
Hibernate: create table compte (id integer not null, numero varchar(255), solde decimal(19,2), 
primary key (id))
Hibernate: create table compte_courant (id integer not null, numero varchar(255), solde 
decimal(19,2), decouvert integer, primary key (id))
Hibernate: create table compte_epargne (id integer not null, numero varchar(255), solde 
decimal(19,2), taux decimal(19,2), primary key (id))
2015-02-11 22:37:46.046 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: select max(ids_.id) from ( select id from compte_courant union select id from 
compte union select id from compte_epargne ) ids_
Hibernate: insert into compte (numero, solde, id) values (?, ?, ?)
Hibernate: insert into compte_courant (numero, solde, decouvert, id) values (?, ?, ?, ?)
Hibernate: insert into compte_epargne (numero, solde, taux, id) values (?, ?, ?, ?)
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert1_0_, compte0_.taux as taux2_0_, compte0_.clazz_ as 
clazz_0_ from ( select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from 
compte union select id, numero, solde, decouvert, null as taux, 1 as clazz_ from 
compte_courant union select id, numero, solde, null as decouvert, taux, 2 as clazz_ from 
compte_epargne ) compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@2c5b4e [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_.decouvert as decouvert1_0_ from compte_courant comptecour0_ 
where comptecour0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteCourant@21999623CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert1_0_, compte0_.taux as taux2_0_, compte0_.clazz_ as 
clazz_0_ from ( select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from 
compte union select id, numero, solde, decouvert, null as taux, 1 as clazz_ from 
compte_courant union select id, numero, solde, null as decouvert, taux, 2 as clazz_ from 
compte_epargne ) compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@7659203CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert1_, compte0_.taux as taux2_, compte0_.clazz_ as clazz_ from ( 
select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from compte union 
select id, numero, solde, decouvert, null as taux, 1 as clazz_ from compte_courant union 
select id, numero, solde, null as decouvert, taux, 2 as clazz_ from compte_epargne ) compte0_
com.jmdoudoux.test.hibernate.entity.Compte@12a81c9 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@24930042CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@5027644CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert1_, compte0_.taux as taux2_, compte0_.clazz_ as clazz_ from ( 
select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from compte union 
select id, numero, solde, decouvert, null as taux, 1 as clazz_ from compte_courant union 
select id, numero, solde, null as decouvert, taux, 2 as clazz_ from compte_epargne ) compte0_ 
where compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.Compte@1a93ff1 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@10814140CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@12935409CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Résultat :
mysql> select * from compte;
+----+--------------+---------+
| id | numero       | solde   |
+----+--------------+---------+
|  1 | 000012345000 |    0.00 |
|  2 | 000012345010 | 1200.00 |
|  3 | 000012345020 | 8000.00 |
|  4 | 000012345000 |    0.00 |
+----+--------------+---------+
4 rows in set (0.00 sec)
mysql> select * from compte_courant;
+----+--------------+---------+-----------+
| id | numero       | solde   | decouvert |
+----+--------------+---------+-----------+
|  5 | 000012345010 | 1200.00 |      2000 |
+----+--------------+---------+-----------+
1 row in set (0.00 sec)
mysql> select * from compte_epargne;
+----+--------------+---------+------+
| id | numero       | solde   | taux |
+----+--------------+---------+------+
|  6 | 000012345020 | 8000.00 | 2.10 |
+----+--------------+---------+------+
1 row in set (0.00 sec)

Il est important que deux tables de classes filles d'une même hiérarchie ne possèdent pas le même identifiant. Hibernate pourrait alors renvoyer plusieurs objets pour un même identifiant en effectuant une union sur les différentes tables. C'est la raison pour laquelle il n'est pas possible d'utiliser la stratégie native pour la génération des identifiants.

Exemple :
       <id name="id" column="id" type="int" >
           <generator class="native"/>
       </id>

Résultat :
org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass>
mapping for: com.jmdoudoux.test.hibernate.entity.CompteEpargne
  at org.hibernate.persister.entity.UnionSubclassEntityPersister.<init>(
  UnionSubclassEntityPersister.java:93)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:
  57)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImp
  l.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
  at org.hibernate.persister.internal.PersisterFactoryImpl.create(PersisterFactoryImpl.java:
  163)
  at org.hibernate.persister.internal.PersisterFactoryImpl.createEntityPersister(
  PersisterFactoryImpl.java:135)
  at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:386)
  at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1744)
  at com.jmdoudoux.test.hibernate.TestHibernate.main(TestHibernate.java:27)
Exception in thread "main" java.lang.NullPointerException
  at com.jmdoudoux.test.hibernate.TestHibernate.main(TestHibernate.java:54)

 

56.11.1.4. XML, une table par sous-classe avec discriminant

La mise en oeuvre de cette stratégie requiert la définition d'un champ qui sera le discriminant dans la classe mère.

Chaque classe fille est mappée en utilisant un tag <subclass> avec deux propriétés :

  • name : précise le nom pleinement qualifié de la classe
  • discriminator_value : précise la valeur qui sera utilisée dans la colonne discriminator

Il faut lui ajouter un tag fils <join> pour définir le mapping de la table avec plusieurs propriétés :

  • table permet de préciser le nom de la table.
  • fetch permet de préciser la stratégie utilisée pour récupérer les données. La valeur select permet de demander à Hibernate de ne pas faire de jointure externe sur la table lors d'une lecture sur la classe mère

Le tag fils <key> permet de préciser le champ qui sera la clé de la table. Il possède plusieurs attributs :

Nom

Rôle

column

Le nom de la colonne qui est la clé étrangère. Optionnel

on-delete

noaction ou cascade. Optionnel, par défaut noaction

property-ref

Préciser un nom de propriété pour indiquer que la clé étrangère ne fait pas référence à la clé primaire de la table mère. Optionnel

not-null

Préciser si la clé étrangère peut être null : true ou false. Optionnel

update

Préciser si la clé étrangère doit être mise à jour : true ou false. Optionnel

unique

Préciser si la clé étrangère doit avoir une contrainte d'unicité : true ou false. Optionnel


Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
 "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.entity.Compte"
    table="compte" discriminator-value="Compte">
    <id name="id" column="id" type="int" >
      <generator class="native" />
    </id>
    <discriminator column="DTYPE" type="string" />
        
    <property name="numero" column="numero" type="string" />
    <property name="solde" column="solde" type="big_decimal" />
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteCourant"
      discriminator-value="CompteCourant">
      <join table="compte_courant"> 
        <key column="compte_courant_id" />
        <property name="decouvert" column="decouvert" type="int"/>
      </join>
    </subclass>
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteEpargne"
      discriminator-value="CompteEpargne">
      <join table="compte_epargne"> 
        <key column="compte_epargne_id" />
        <property name="taux" column="taux" type="big_decimal"/>
      </join>
    </subclass>      
  </class>
</hibernate-mapping>

Résultat :
2015-02-03 21:59:56.856 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-02-03 21:59:56.864 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-02-03 21:59:56.866 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-02-03 21:59:56.868 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-02-03 21:59:56.895 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-02-03 21:59:56.895 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-02-03 21:59:56.943 INFO   [main]:org.hibernate.cfg.Configuration - HHH000221: Reading 
mappings from resource: Compte.hbm.xml
2015-02-03 21:59:57.000 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-02-03 21:59:57.104 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not
for production use!)
2015-02-03 21:59:57.115 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-02-03 21:59:57.116 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-02-03 21:59:57.116 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL 
[jdbc:mysql://localhost:3307/mabdd]
2015-02-03 21:59:57.117 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-02-03 21:59:57.530 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-02-03 21:59:57.552 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-02-03 21:59:57.557 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-02-03 21:59:57.897 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: alter table compte_courant drop foreign key FK2435FDBFBBF1A400
2015-02-03 21:59:57.915 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000389: 
Unsuccessful: alter table compte_courant drop foreign key FK2435FDBFBBF1A400
2015-02-03 21:59:57.915 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - Table 
'mabdd.compte_courant' doesn't exist
Hibernate: alter table compte_epargne drop foreign key FK8E9D8D438FCF4580
2015-02-03 21:59:57.917 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000389: 
Unsuccessful: alter table compte_epargne drop foreign key FK8E9D8D438FCF4580
2015-02-03 21:59:57.917 ERROR  [main]:org.hibernate.tool.hbm2ddl.SchemaExport - Table 'mabdd.
compte_epargne' doesn't exist
Hibernate: drop table if exists compte
Hibernate: drop table if exists compte_courant
Hibernate: drop table if exists compte_epargne
Hibernate: create table compte (id integer not null auto_increment, DTYPE varchar(255) not 
null, numero varchar(255), solde decimal(19,2), primary key (id))
Hibernate: create table compte_courant (compte_courant_id integer not null, decouvert integer
, primary key (compte_courant_id))
Hibernate: create table compte_epargne (compte_epargne_id integer not null, taux decimal(19,2)
, primary key (compte_epargne_id))
Hibernate: alter table compte_courant add index FK2435FDBFBBF1A400 (compte_courant_id), add 
constraint FK2435FDBFBBF1A400 foreign key (compte_courant_id) references compte (id)
Hibernate: alter table compte_epargne add index FK8E9D8D438FCF4580 (compte_epargne_id), add 
constraint FK8E9D8D438FCF4580 foreign key (compte_epargne_id) references compte (id)
2015-02-03 21:59:58.088 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: insert into compte (numero, solde, DTYPE) values (?, ?, 'Compte')
Hibernate: insert into compte (numero, solde, DTYPE) values (?, ?, 'CompteCourant')
Hibernate: insert into compte_courant (decouvert, compte_courant_id) values (?, ?)
Hibernate: insert into compte (numero, solde, DTYPE) values (?, ?, 'CompteEpargne')
Hibernate: insert into compte_epargne (taux, compte_epargne_id) values (?, ?)

Résultat :
CREATE TABLE `compte` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `DTYPE` varchar(255) NOT NULL,
  `numero` varchar(255) DEFAULT NULL,
  `solde` decimal(19,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `compte_courant` (
  `compte_courant_id` int(11) NOT NULL,
  `decouvert` int(11) DEFAULT NULL,
  PRIMARY KEY (`compte_courant_id`),
  KEY `FK2435FDBFBBF1A400` (`compte_courant_id`),
  CONSTRAINT `FK2435FDBFBBF1A400` FOREIGN KEY (`compte_courant_id`) REFERENCES `compte` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `compte_epargne` (
  `compte_epargne_id` int(11) NOT NULL,
  `taux` decimal(19,2) DEFAULT NULL,
  PRIMARY KEY (`compte_epargne_id`),
  KEY `FK8E9D8D438FCF4580` (`compte_epargne_id`),
  CONSTRAINT `FK8E9D8D438FCF4580` FOREIGN KEY (`compte_epargne_id`) REFERENCES `compte` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Résultat :
mysql> select * from compte;
+----+---------------+--------------+---------+
| id | DTYPE         | numero       | solde   |
+----+---------------+--------------+---------+
|  1 | Compte        | 000012345000 |    0.00 |
|  2 | CompteCourant | 000012345010 | 1200.00 |
|  3 | CompteEpargne | 000012345020 | 8000.00 |
+----+---------------+--------------+---------+
3 rows in set (0.00 sec)
mysql> select * from compte_courant;
+-------------------+-----------+
| compte_courant_id | decouvert |
+-------------------+-----------+
|                 2 |      2000 |
+-------------------+-----------+
1 row in set (0.00 sec)
mysql> select * from compte_epargne;
+-------------------+------+
| compte_epargne_id | taux |
+-------------------+------+
|                 3 | 2.10 |
+-------------------+------+
1 row in set (0.00 sec)

Résultat :
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, compte0_.DTYPE 
as DTYPE0_0_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.compte_courant_id left outer join compte_epargne compte0_2_ on compte0_.id=
compte0_2_.compte_epargne_id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@422ddc [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_1_.decouvert as decouvert1_0_ from compte comptecour0_ inner 
join compte_courant comptecour0_1_ on comptecour0_.id=comptecour0_1_.compte_courant_id where 
comptecour0_.id=? and comptecour0_.DTYPE='CompteCourant'
com.jmdoudoux.test.hibernate.entity.CompteCourant@32800355CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, compte0_.DTYPE 
as DTYPE0_0_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.compte_courant_id left outer join compte_epargne compte0_2_ on compte0_.id=
compte0_2_.compte_epargne_id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@29827422CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_1_.decouvert as decouvert1_, compte0_2_.taux as taux2_, compte0_.DTYPE as DTYPE0_ from 
compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=compte0_1_.
compte_courant_id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.
compte_epargne_id where compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.CompteCourant@27737342CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@31801703CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
com.jmdoudoux.test.hibernate.entity.Compte@28e5a7 [id=1, numero=000012345000, solde=0.00]

Lors de la recherche des différentes entités, une seule requête SQL est exécutée à chaque fois requérant une jointure entre au moins deux tables voire toutes les tables. Ceci peut poser des problèmes de performance notamment lorsque le nombre de tables filles est plus important.

Pour palier partiellement cette problématique, il est possible de changer la stratégie de récupération des données des tables filles en demandant à Hibernate de ne pas faire de jointure mais de faire des requêtes SQL dédiées. Pour cela, il faut utiliser l'attribut fetch="select" du tag <join>.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
 "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernate.entity.Compte"
    table="compte" discriminator-value="Compte">
    <id name="id" column="id" type="int" >
      <generator class="native" />
    </id>
    <discriminator column="DTYPE" type="string" />
        
    <property name="numero" column="numero" type="string" />
    <property name="solde" column="solde" type="big_decimal" />
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteCourant"
      discriminator-value="CompteCourant">
      <join table="compte_courant" fetch="select">
         <key column="compte_courant_id" />
         <property name="decouvert" column="decouvert" type="int"/>
      </join>
    </subclass>
    <subclass name="com.jmdoudoux.test.hibernate.entity.CompteEpargne"
      discriminator-value="CompteEpargne">
      <join table="compte_epargne" fetch="select">
        <key column="compte_epargne_id" />
        <property name="taux" column="taux" type="big_decimal"/>
      </join>
    </subclass>      
  </class>
</hibernate-mapping>

Résultat :
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.DTYPE as DTYPE0_0_ from compte compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@e15c54 [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_1_.decouvert as decouvert1_0_ from compte comptecour0_ inner 
join compte_courant comptecour0_1_ on comptecour0_.id=comptecour0_1_.compte_courant_id where 
comptecour0_.id=? and comptecour0_.DTYPE='CompteCourant'
com.jmdoudoux.test.hibernate.entity.CompteCourant@24177821CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.DTYPE as DTYPE0_0_ from compte compte0_ where compte0_.id=?
Hibernate: select compte_2_.taux as taux2_ from compte_epargne compte_2_ where compte_2_.
compte_epargne_id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@11010466CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.DTYPE as DTYPE0_ from compte compte0_ where compte0_.numero like ?
Hibernate: select compte_1_.decouvert as decouvert1_ from compte_courant compte_1_ where 
compte_1_.compte_courant_id=?
Hibernate: select compte_2_.taux as taux2_ from compte_epargne compte_2_ where compte_2_.
compte_epargne_id=?
com.jmdoudoux.test.hibernate.entity.Compte@476e01 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@11305489CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@15939468CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Ce sont des requêtes SQL supplémentaires dont le nombre peut devenir important en fonction des entités retournées mais ces requêtes se font sur la clé primaire des tables filles.

 

56.11.2. Annotations

Comme Hibernate est une implémentation de JPA, il est possible de définir le mapping en utilisant des annotations.

Le fichier de configuration ci-dessous est utilisé dans les exemples de cette section.

Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3307/mabdd</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">root</property>
    <property name="hibernate.connection.pool_size">1</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Cache de second niveau désactivé  -->
    <property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property>
    <!-- Afficher les requêtes SQL exécutées sur la sortie standard -->
    <property name="hibernate.show_sql">true</property>
    <property name="hbm2ddl.auto">create</property>
    <mapping class="com.jmdoudoux.test.hibernate.entity.Compte"/>
    <mapping class="com.jmdoudoux.test.hibernate.entity.CompteCourant"/>
    <mapping class="com.jmdoudoux.test.hibernate.entity.CompteEpargne"/>
  </session-factory>
</hibernate-configuration>

Deux classes filles qui héritent de la classe Compte sont définies en tant qu'entités.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "compte_courant")
public class CompteCourant extends Compte {
  
  @Column(name = "decouvert")
  private int decouvert;
  
  public int getDecouvert() {
    return decouvert;
  }
  
  public void setDecouvert(int decouvert) {
    this.decouvert = decouvert;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteCourant [id=" + id + ", numero=" + numero + ", solde=" + solde
        + ", decouvert=" + decouvert + "]";
  }
}

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "compte_epargne")
public class CompteEpargne extends Compte {
  @Column(name = "taux")
  private BigDecimal taux;
  
  public BigDecimal getTaux() {
    return taux;
  }
  
  public void setTaux(BigDecimal taux) {
    this.taux = taux;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteEpargne [ id=" + id + ", numero=" + numero + ", solde="
        + solde + ", taux=" + taux + "]";
  }
}

 

56.11.2.1. Annotations, une table par hiérarchie de classes (SINGLE_TABLE)

L'entité de la classe mère utilise la stratégie InheritanceType.SINGLE_TABLE grâce à l'annotation @Inheritance.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "compte")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Compte {
  
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  @Column(name = "id")
  protected int        id;
  
  @Column(name = "numero")
  protected String     numero;
  
  @Column(name = "solde")
  protected BigDecimal solde;
  
  public int getId() {
    return id;
  }
  
  public void setId(int id) {
    this.id = id;
  }
  
  public String getNumero() {
    return numero;
  }
  
  public void setNumero(String numero) {
    this.numero = numero;
  }
  
  public BigDecimal getSolde() {
    return solde;
  }
  
  public void setSolde(BigDecimal solde) {
    this.solde = solde;
  }
  
  @Override
  public String toString() {
    return super.toString() + " [id=" + id + ", numero=" + numero + ", solde="
        + solde + "]";
  }
}

Résultat :
2015-03-03 23:00:59.778 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-03-03 23:00:59.786 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-03-03 23:00:59.788 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-03-03 23:00:59.790 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-03-03 23:00:59.818 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-03-03 23:00:59.818 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-03-03 23:00:59.874 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-03-03 23:01:00.042 WARN   [main]:org.hibernate.cfg.AnnotationBinder - HHH000139: Illegal 
use of @Table in a subclass of a SINGLE_TABLE hierarchy: com.jmdoudoux.test.hibernate.entity.
CompteCourant
2015-03-03 23:01:00.043 WARN   [main]:org.hibernate.cfg.AnnotationBinder - HHH000139: Illegal 
use of @Table in a subclass of a SINGLE_TABLE hierarchy: com.jmdoudoux.test.hibernate.entity.
CompteEpargne
2015-03-03 23:01:00.056 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-03-03 23:01:00.063 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-03-03 23:01:00.063 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-03-03 23:01:00.064 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-03-03 23:01:00.064 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-03-03 23:01:00.367 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-03-03 23:01:00.397 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-03-03 23:01:00.402 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-03-03 23:01:00.671 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: drop table if exists compte
Hibernate: drop table if exists hibernate_sequences
Hibernate: create table compte (DTYPE varchar(31) not null, id integer not null, numero 
varchar(255), solde decimal(19,2), decouvert integer, taux decimal(19,2), primary key (id))
Hibernate: create table hibernate_sequences ( sequence_name varchar(255),  
sequence_next_hi_value integer ) 
2015-03-03 23:01:00.722 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: select sequence_next_hi_value from hibernate_sequences where sequence_name = '
compte' for update
Hibernate: insert into hibernate_sequences(sequence_name, sequence_next_hi_value) values('
compte', ?)
Hibernate: update hibernate_sequences set sequence_next_hi_value = ? where 
sequence_next_hi_value = ? and sequence_name = 'compte'
Hibernate: insert into compte (numero, solde, DTYPE, id) values (?, ?, 'Compte', ?)
Hibernate: insert into compte (numero, solde, decouvert, DTYPE, id) values (?, ?, ?, '
CompteCourant', ?)
Hibernate: insert into compte (numero, solde, taux, DTYPE, id) values (?, ?, ?, 'CompteEpargne'
, ?)
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert0_0_, compte0_.taux as taux0_0_, compte0_.DTYPE as 
DTYPE0_0_ from compte compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@af908f [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_.decouvert as decouvert0_0_ from compte comptecour0_ where 
comptecour0_.id=? and comptecour0_.DTYPE='CompteCourant'
com.jmdoudoux.test.hibernate.entity.CompteCourant@27723440CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert0_0_, compte0_.taux as taux0_0_, compte0_.DTYPE as 
DTYPE0_0_ from compte compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@20700084CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert0_, compte0_.taux as taux0_, compte0_.DTYPE as DTYPE0_ from 
compte compte0_
com.jmdoudoux.test.hibernate.entity.Compte@164fc53 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@13942651CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@23542324CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert0_, compte0_.taux as taux0_, compte0_.DTYPE as DTYPE0_ from 
compte compte0_ where compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.Compte@6ca594 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@15384254CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@4681268CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Résultat :
mysql> select * from compte;
+---------------+----+--------------+---------+-----------+------+
| DTYPE         | id | numero       | solde   | decouvert | taux |
+---------------+----+--------------+---------+-----------+------+
| Compte        |  1 | 000012345000 |    0.00 |      NULL | NULL |
| CompteCourant |  2 | 000012345010 | 1200.00 |      2000 | NULL |
| CompteEpargne |  3 | 000012345020 | 8000.00 |      NULL | 2.10 |
+---------------+----+--------------+---------+-----------+------+
3 rows in set (0.00 sec)

 

56.11.2.2. Annotations, une table par classe concrète (TABLE_PER_CLASS)

Hibernate propose un support de la stratégie une table par classe concrète qui est définie comme étant optionnelle par JPA. Pour cela, il suffit d'utiliser sur la classe mère l'annotation @Inheritance avec la propriété strategy initialisée avec InheritanceType.TABLE_PER_CLASS.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "compte")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Compte {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  @Column(name = "id")
  protected int        id;
  
  @Column(name = "numero")
  protected String     numero;
  
  @Column(name = "solde")
  protected BigDecimal solde;
  
  public int getId() {
    return id;
  }
  
  public void setId(int id) {
    this.id = id;
  }
  
  public String getNumero() {
    return numero;
  }
  
  public void setNumero(String numero) {
    this.numero = numero;
  }
  
  public BigDecimal getSolde() {
    return solde;
  }
  
  public void setSolde(BigDecimal solde) {
    this.solde = solde;
  }
  
  @Override
  public String toString() {
    return super.toString() + " [id=" + id + ", numero=" + numero + ", solde="
        + solde + "]";
  }
}

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "compte_courant")
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "id")),
    @AttributeOverride(name = "numero", column = @Column(name = "numero")),
    @AttributeOverride(name = "solde", column = @Column(name = "solde")) })
public class CompteCourant extends Compte {
  @Column(name = "decouvert")
  private int decouvert;
  
  public int getDecouvert() {
    return decouvert;
  }
  
  public void setDecouvert(int decouvert) {
    this.decouvert = decouvert;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteCourant [id=" + id + ", numero=" + numero + ", solde=" + solde
        + ", decouvert=" + decouvert + "]";
  }
}

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import java.math.BigDecimal;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "compte_epargne")
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "id")),
    @AttributeOverride(name = "numero", column = @Column(name = "numero")),
    @AttributeOverride(name = "solde", column = @Column(name = "solde")) })
public class CompteEpargne extends Compte {
  @Column(name = "taux")
  private BigDecimal taux;
  
  public BigDecimal getTaux() {
    return taux;
  }
  
  public void setTaux(BigDecimal taux) {
    this.taux = taux;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteEpargne [ id=" + id + ", numero=" + numero + ", solde="
        + solde + ", taux=" + taux + "]";
  }
}

Dans les exemples ci-dessus, les annotations @AttributesOverrides et @AttributesOverride sont employées à titre indicatif car elles utilisent les valeurs par défaut et pourraient donc être retirées.

Résultat :
2015-03-03 23:18:12.766 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-03-03 23:18:12.776 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-03-03 23:18:12.779 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-03-03 23:18:12.780 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-03-03 23:18:12.814 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-03-03 23:18:12.814 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-03-03 23:18:12.891 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-03-03 23:18:13.080 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-03-03 23:18:13.087 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-03-03 23:18:13.088 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-03-03 23:18:13.088 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-03-03 23:18:13.089 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-03-03 23:18:13.392 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-03-03 23:18:13.424 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-03-03 23:18:13.428 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-03-03 23:18:13.736 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: drop table if exists compte
Hibernate: drop table if exists compte_courant
Hibernate: drop table if exists compte_epargne
Hibernate: drop table if exists hibernate_sequences
Hibernate: create table compte (id integer not null, numero varchar(255), solde decimal(19,2), 
primary key (id))
Hibernate: create table compte_courant (id integer not null, numero varchar(255), solde 
decimal(19,2), decouvert integer, primary key (id))
Hibernate: create table compte_epargne (id integer not null, numero varchar(255), solde 
decimal(19,2), taux decimal(19,2), primary key (id))
Hibernate: create table hibernate_sequences ( sequence_name varchar(255),  
sequence_next_hi_value integer ) 
2015-03-03 23:18:13.843 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: select sequence_next_hi_value from hibernate_sequences where sequence_name = '
compte' for update
Hibernate: insert into hibernate_sequences(sequence_name, sequence_next_hi_value) values('
compte', ?)
Hibernate: update hibernate_sequences set sequence_next_hi_value = ? where 
sequence_next_hi_value = ? and sequence_name = 'compte'
Hibernate: insert into compte (numero, solde, id) values (?, ?, ?)
Hibernate: insert into compte_courant (numero, solde, decouvert, id) values (?, ?, ?, ?)
Hibernate: insert into compte_epargne (numero, solde, taux, id) values (?, ?, ?, ?)
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert1_0_, compte0_.taux as taux2_0_, compte0_.clazz_ as 
clazz_0_ from ( select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from 
compte union select id, numero, solde, decouvert, null as taux, 1 as clazz_ from 
compte_courant union select id, numero, solde, null as decouvert, taux, 2 as clazz_ from 
compte_epargne ) compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@198f6dd [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_.numero as numero0_0_, comptecour0_.
solde as solde0_0_, comptecour0_.decouvert as decouvert1_0_ from compte_courant comptecour0_ 
where comptecour0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteCourant@3316864CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_.decouvert as decouvert1_0_, compte0_.taux as taux2_0_, compte0_.clazz_ as 
clazz_0_ from ( select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from 
compte union select id, numero, solde, decouvert, null as taux, 1 as clazz_ from 
compte_courant union select id, numero, solde, null as decouvert, taux, 2 as clazz_ from 
compte_epargne ) compte0_ where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@20376252CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert1_, compte0_.taux as taux2_, compte0_.clazz_ as clazz_ from ( 
select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from compte union 
select id, numero, solde, decouvert, null as taux, 1 as clazz_ from compte_courant union 
select id, numero, solde, null as decouvert, taux, 2 as clazz_ from compte_epargne ) compte0_
com.jmdoudoux.test.hibernate.entity.Compte@165d7a2 [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@30230592CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@4358252CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_.decouvert as decouvert1_, compte0_.taux as taux2_, compte0_.clazz_ as clazz_ from ( 
select id, numero, solde, null as decouvert, null as taux, 0 as clazz_ from compte union 
select id, numero, solde, decouvert, null as taux, 1 as clazz_ from compte_courant union 
select id, numero, solde, null as decouvert, taux, 2 as clazz_ from compte_epargne ) compte0_ 
where compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.Compte@129d5bf [id=1, numero=000012345000, solde=0.00]
com.jmdoudoux.test.hibernate.entity.CompteCourant@26225329CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@11121275CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Résultat :
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 73
Server version: 5.6.15 MySQL Community Server (GPL)

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates.
Other names may be trademarks of their respective owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use mabdd
Database changed
mysql> select * from compte;
+----+--------------+-------+
| id | numero       | solde |
+----+--------------+-------+
|  1 | 000012345000 |  0.00 |
+----+--------------+-------+
1 row in set (0.02 sec)
mysql> select * from compte_courant;
+----+--------------+---------+-----------+
| id | numero       | solde   | decouvert |
+----+--------------+---------+-----------+
|  2 | 000012345010 | 1200.00 |      2000 |
+----+--------------+---------+-----------+
1 row in set (0.00 sec)
mysql> select * from compte_epargne;
+----+--------------+---------+------+
| id | numero       | solde   | taux |
+----+--------------+---------+------+
|  3 | 000012345020 | 8000.00 | 2.10 |
+----+--------------+---------+------+
1 row in set (0.00 sec)
mysql>

 

56.11.2.3. Annotations, une table par sous-classe (JOINED)

L'entité de la classe mère utilise la stratégie InheritanceType.JOINED grâce à l'annotation @Inheritance.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;

import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "compte")
@Inheritance(strategy =
InheritanceType.JOINED)
public class Compte {
  @Id
  @GeneratedValue
  @Column(name = "id")
  protected int        id;

  @Column(name = "numero")
  protected String     numero;

  @Column(name = "solde")
  protected BigDecimal solde;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getNumero() {
    return numero;
  }

  public void setNumero(String numero) {
    this.numero = numero;
  }

  public BigDecimal getSolde() {
    return solde;
  }
  
  public void setSolde(BigDecimal solde) {
    this.solde = solde;
  }

  @Override
  public String toString() {
    return super.toString() + " [id="+ id + ", numero=" + numero + ", solde="
        + solde + "]";
  }
}

L'annotation @PrimaryKeyJoinColumn est utilisée sur les classes filles pour préciser la colonne qui est la clé primaire et qui servira de clé étrangère lors de la jointure avec la table de la classe mère.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;
      
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "compte_courant")
@PrimaryKeyJoinColumn(name = "id")
public class CompteCourant extends Compte {
  @Column(name = "decouvert")
  private int decouvert;
  
  public int getDecouvert() {
    return decouvert;
  }
  
  public void setDecouvert(int decouvert){
    this.decouvert = decouvert;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteCourant [id=" + id + ", numero=" + numero + ", solde=" + solde
        + ", decouvert=" + decouvert + "]";
  }
}

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.hibernate.entity;

import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "compte_epargne")
@PrimaryKeyJoinColumn(name = "id")
public class CompteEpargne extends Compte {
  @Column(name = "taux")
  private BigDecimal taux;

  public BigDecimal getTaux() {
    return taux;
  }

  public void setTaux(BigDecimal taux) {
    this.taux = taux;
  }
  
  @Override
  public String toString() {
    return this.getClass().getName() + "@" + System.identityHashCode(this)
        + "CompteEpargne [ id=" + id + ", numero=" + numero + ", solde="
        + solde + ", taux=" + taux + "]";
  }
}

Résultat :
2015-03-05 21:04:07.364 INFO   [main]:org.hibernate.annotations.common.Version - HCANN000001: 
Hibernate Commons Annotations {4.0.1.Final}
2015-03-05 21:04:07.373 INFO   [main]:org.hibernate.Version - HHH000412: Hibernate Core {4.1.
4.Final}
2015-03-05 21:04:07.375 INFO   [main]:org.hibernate.cfg.Environment - HHH000206: hibernate.
properties not found
2015-03-05 21:04:07.377 INFO   [main]:org.hibernate.cfg.Environment - HHH000021: Bytecode 
provider name : javassist
2015-03-05 21:04:07.405 INFO   [main]:org.hibernate.cfg.Configuration - HHH000043: 
Configuring from resource: /hibernate.cfg.xml
2015-03-05 21:04:07.405 INFO   [main]:org.hibernate.cfg.Configuration - HHH000040: 
Configuration resource: /hibernate.cfg.xml
2015-03-05 21:04:07.463 INFO   [main]:org.hibernate.cfg.Configuration - HHH000041: Configured 
SessionFactory: null
2015-03-05 21:04:07.641 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not 
for production use!)
2015-03-05 21:04:07.648 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 1
2015-03-05 21:04:07.648 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000006: Autocommit mode: false
2015-03-05 21:04:07.649 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000401: using driver [com.mysql.jdbc.Driver] at URL [
jdbc:mysql://localhost:3307/mabdd]
2015-03-05 21:04:07.649 INFO   [main]:org.hibernate.service.jdbc.connections.internal.
DriverManagerConnectionProviderImpl - HHH000046: Connection properties: {user=root, password=
****}
2015-03-05 21:04:07.955 INFO   [main]:org.hibernate.dialect.Dialect - HHH000400: Using 
dialect: org.hibernate.dialect.MySQL5Dialect
2015-03-05 21:04:07.983 INFO   [main]:org.hibernate.engine.transaction.internal.
TransactionFactoryInitiator - HHH000399: Using default transaction strategy (direct JDBC 
transactions)
2015-03-05 21:04:07.988 INFO   [main]:org.hibernate.hql.internal.ast.
ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
2015-03-05 21:04:08.211 INFO   [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000227: 
Running hbm2ddl schema export
Hibernate: alter table compte_courant drop foreign key FK2435FDBF9CCD1BB4
Hibernate: alter table compte_epargne drop foreign key FK8E9D8D439CCD1BB4
Hibernate: drop table if exists compte
Hibernate: drop table if exists compte_courant
Hibernate: drop table if exists compte_epargne
Hibernate: create table compte (id integer not null auto_increment, numero varchar(255), solde 
decimal(19,2), primary key (id))
Hibernate: create table compte_courant (decouvert integer, id integer not null, primary key (
id))
Hibernate: create table compte_epargne (taux decimal(19,2), id integer not null, primary key (
id))
Hibernate: alter table compte_courant add index FK2435FDBF9CCD1BB4 (id), add constraint 
FK2435FDBF9CCD1BB4 foreign key (id) references compte (id)
Hibernate: alter table compte_epargne add index FK8E9D8D439CCD1BB4 (id), add constraint 
FK8E9D8D439CCD1BB4 foreign key (id) references compte (id)
2015-03-05 21:04:08.315 INFO     [main]:org.hibernate.tool.hbm2ddl.SchemaExport - HHH000230: 
Schema export complete
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte_courant (decouvert, id) values (?, ?)
Hibernate: insert into compte (numero, solde) values (?, ?)
Hibernate: insert into compte_epargne (taux, id) values (?, ?)
Recherche d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, case when 
compte0_1_.id is not null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not 
null then 0 end as clazz_0_ from compte compte0_ left outer join compte_courant compte0_1_ on 
compte0_.id=compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.
id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.Compte@24ad9b [id=1, numero=000012345000, solde=0.00]

Recherche d'un compte courant
Hibernate: select comptecour0_.id as id0_0_, comptecour0_1_.numero as numero0_0_, 
comptecour0_1_.solde as solde0_0_, comptecour0_.decouvert as decouvert1_0_ from compte_courant 
comptecour0_ inner join compte comptecour0_1_ on comptecour0_.id=comptecour0_1_.id where 
comptecour0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteCourant@29984851CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]

Recherche polymorphique d'un compte
Hibernate: select compte0_.id as id0_0_, compte0_.numero as numero0_0_, compte0_.solde as 
solde0_0_, compte0_1_.decouvert as decouvert1_0_, compte0_2_.taux as taux2_0_, case when 
compte0_1_.id is not null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not 
null then 0 end as clazz_0_ from compte compte0_ left outer join compte_courant compte0_1_ on 
compte0_.id=compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.
id where compte0_.id=?
com.jmdoudoux.test.hibernate.entity.CompteEpargne@6285813CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]

Recherche de tous les comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_1_.decouvert as decouvert1_, compte0_2_.taux as taux2_, case when compte0_1_.id is not 
null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not null then 0 end as 
clazz_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.id
com.jmdoudoux.test.hibernate.entity.CompteCourant@15462959CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@12544708CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
com.jmdoudoux.test.hibernate.entity.Compte@fa8f2c [id=1, numero=000012345000, solde=0.00]

Recherche polymorphique de comptes
Hibernate: select compte0_.id as id0_, compte0_.numero as numero0_, compte0_.solde as solde0_, 
compte0_1_.decouvert as decouvert1_, compte0_2_.taux as taux2_, case when compte0_1_.id is not 
null then 1 when compte0_2_.id is not null then 2 when compte0_.id is not null then 0 end as 
clazz_ from compte compte0_ left outer join compte_courant compte0_1_ on compte0_.id=
compte0_1_.id left outer join compte_epargne compte0_2_ on compte0_.id=compte0_2_.id where 
compte0_.numero like ?
com.jmdoudoux.test.hibernate.entity.CompteCourant@9037617CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
com.jmdoudoux.test.hibernate.entity.CompteEpargne@27879211CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
com.jmdoudoux.test.hibernate.entity.Compte@1a4654d [id=1, numero=000012345000, solde=0.00]

Résultat :
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 82
Server version: 5.6.15 MySQL Community Server (GPL)

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use mabdd
Database changed
mysql> select * from compte;
+----+--------------+---------+
| id | numero       | solde   |
+----+--------------+---------+
|  1 | 000012345000 |    0.00 |
|  2 | 000012345010 | 1200.00 |
|  3 | 000012345020 | 8000.00 |
+----+--------------+---------+
3 rows in set (0.00 sec)
mysql> select * from compte_courant;
+-----------+----+
| decouvert | id |
+-----------+----+
|      2000 |  2 |
+-----------+----+
1 row in set (0.00 sec)
mysql> select * from compte_epargne;
+------+----+
| taux | id |
+------+----+
| 2.10 |  3 |
+------+----+
1 row in set (0.00 sec)
mysql>

 

56.12. Les caches d'Hibernate

Un volume important d'échanges entre l'application et la base de données est fréquemment à l'origine des problèmes de performance d'une application.

Le but d'un cache est de réduire les échanges entre l'application et la base de données en stockant en mémoire les entités déjà lues de la base de données. Une lecture dans la base de données n'est alors nécessaire que lorsque l'entité n'est pas déjà présente dans le cache (si l'entité n'a jamais été lue ou si l'entité a été retirée du cache).

Le cache permet de stocker des données qui ont déjà été lues de la base de données, ce qui permet de réduire les échanges entre la base de données et l'application lorsque celle-ci a de nouveau besoin des données. L'accès à la mémoire est beaucoup plus rapide que l'accès à la base de données surtout si celui-ci nécessite un échange utilisant le réseau.

Hibernate propose une fonctionnalité de mise en cache des données qui peut permettre, si elle est correctement configurée et utilisée, d'améliorer les performances de l'application. Il est nécessaire de bien comprendre le mode de fonctionnement du cache pour l'utiliser à bon escient et le configurer en conséquence : dans le cas contraire, les performances peuvent se dégrader de façon sérieuse voire dramatique.

L'utilisation du cache peut donc être une solution à certaines problématiques de performance fréquemment rencontrées lorsqu'Hibernate n'est pas bien compris ou n'est pas utilisé de manière optimale. Même si ces deux critères sont parfaitement maîtrisés, le cache peut être utile pour améliorer les performances dans certaines circonstances notamment lors de la lecture de données statiques dans la base de données.

Hibernate propose d'utiliser un cache pour plusieurs types de fonctionnalités de manière à améliorer les performances en lecture et/ou écriture des entités dans la base de données :

  • cache de premier niveau : son utilisation est implicite car il est toujours actif et est utilisé par défaut. Implémenté dans la session, son champ d'action est limité à la transaction courante
  • cache de second niveau : son utilisation est optionnelle ; il doit être activé et configuré pour pouvoir être utilisé. Implémenté dans l'objet de type SessionFactory, son champ d'action est l'application : il est donc utilisable par toutes les transactions
  • le cache des requêtes : son utilisation est optionnelle ; il doit être activé et configuré pour pouvoir être utilisé. Sa mise en oeuvre utilise le cache de second niveau.

Une bonne connaissance de la façon dont ces caches fonctionnent et de la manière dont ils interagissent avec les autres API est essentielle pour obtenir les meilleurs résultats. Activer le cache et configurer les entités est facile mais cela ne garantit pas de pouvoir tirer le meilleur parti des fonctionnalités proposées par les caches d'Hibernate.

Un objet de type Session est une unité de travail, qui correspond à une transaction côté base de données : elle stocke l'état sur les entités suite à des lectures et/ou modifications et/ou suppressions. Ces modifications et suppressions sont transformées en requêtes SQL pour être reportées dans la base de données. Le stockage de cet état représente le premier niveau de cache d'Hibernate.

Dans le cache de premier niveau, implémenté dans la session, les entités sont directement stockées dans le cache. Si une entité est de nouveau récupérée de la session alors qu'elle a déjà été chargée c'est le même objet encapsulant l'entité qui est retourné. Ceci ne pose aucun problème puisque la portée de la session est la transaction courante. Ce mode de fonctionnement est utilisé jusqu'à ce que la transaction soit terminée ou que la méthode flush de la session soit invoquée.

Le cache de premier niveau est essentiellement utilisé pour limiter le nombre de requêtes SQL requises par la transaction : par exemple, si une entité est modifiée plusieurs fois durant la transaction, l'état final de l'entité est stocké dans la session qui ne générera qu'une seule requête SQL de type update, par défaut à la fin de la transaction.

Le cache de second niveau stocke les objets lus de la base de données au niveau de l'objet SessionFactory : les entités sont donc partagées entre les transactions et utilisables au niveau de l'application. Dans ce cas, lorsqu'une transaction a besoin de lire une entité, si celle-ci est déjà présente dans le cache alors l'exécution d'une ou plusieurs requêtes SQL est évitée.

L'utilisation du cache de second niveau est optionnelle. La durée de vie du cache de second niveau est liée à celle de l'application. La durée de vie des entités contenues dans le cache est configurable selon le cache utilisé. Le cache de second niveau requiert l'utilisation d'une implémentation d'un cache par un tiers.

Par défaut, si le cache de second niveau est activé, la recherche d'une entité se fait prioritairement dans le cache de premier niveau, puis dans le cache de second niveau et enfin dans la base de données par l'exécution d'une requête SQL.

Hibernate propose une fonctionnalité qui permet de mettre en cache le résultat de requêtes SQL. Cette fonctionnalité possède des contraintes mais peut être intéressante dans certains cas de figure.

 

56.12.1. Des recommandations pour l'utilisation des caches

La bonne compréhension du mode de fonctionnement et de la configuration des caches d'Hibernate est importante pour permettre d'en tirer le meilleur parti. C'est notamment le cas du cache de second niveau qui peut, en étant correctement configuré et utilisé, améliorer les performances d'Hibernate.

Attention : l'utilisation d'un cache ne peut pas être l'unique solution aux problèmes de performance avec Hibernate. Comme pour d'autres besoins, le cache peut être une solution pour certaines problématiques de performances mais il induit aussi d'autres soucis (fraicheur des données, durée avant l'invalidation des données, réplication des données dans un cluster, ...). De plus, mal utilisé ou configuré, l'utilisation du cache peut aussi dégrader les performances ou poser des problèmes d'accès concurrents.

La gestion de la fraicheur des données est une problématique fréquente lors de l'utilisation d'un cache mais elle devient cruciale dans le cas du cache des entités d'Hibernate.

Généralement par méconnaissance, Hibernate est souvent perçu comme peu performant car générateur de nombreuses requêtes SQL. Il est important de connaître son mode de fonctionnement afin de s'y adapter pour obtenir les meilleurs résultats. Sa première approche est plutôt facile mais elle masque aux développeurs un moteur complexe dont la connaissance du fonctionnement est requise pour ne pas être déçu et même pour ne pas avoir de gros problèmes d'utilisation généralement relatifs à la performance.

L'activation du cache de second niveau est facile mais il est très important de bien comprendre comment il fonctionne dans les situations où il peut s'appliquer pour éviter d'avoir un surcoût lié à l'utilisation du cache sans en obtenir les bénéfices.

Il est aussi important de noter que les caches ne sont jamais informés des modifications qui sont faites sur les données de la base par des applications tierces. Par exemple, Hibernate n'est pas capable de savoir si une donnée mise en cache est modifiée par une autre application ou par la base de données elle-même (exécution de procédures stockées ou de triggers).

Si ce cas de figure se présente, il est nécessaire de configurer la région pour qu'elle s'invalide périodiquement dans un délais plutôt court ceci afin de régulièrement retirer les données qu'elle contient. Cette périodicité est à définir selon les besoins.

 

56.12.2. Les différents caches d'Hibernate

Plusieurs actions peuvent faire réaliser une requête de type SQL par Hibernate :

  • obtenir une entité par son identifiant, par exemple en utilisant la méthode find() ou get()
  • obtenir une entité lors du parcours d'une relation puisque par défaut seuls leurs identifiants sont encapsulés dans un objet de type proxy
  • obtenir une entité en utilisant une requête HQL ou l'API Criteria

L'utilisation d'un cache a pour but d'améliorer les performances : il se place entre l'application et la base de données pour stocker des données et ainsi éviter de les rechercher systématiquement de la base de données. L'objectif principal des caches est de limiter la répétition des requêtes de type select en stockant les entités lues pour les obtenir du cache lors des invocations suivantes.

Hibernate propose deux types de caches, chacun ayant un but précis :

  • le cache de niveau 1 ou cache de premier niveau : il est toujours actif car il est implémenté dans la classe Session. La visibilité de ce cache est la transaction. Sa durée de vie est donc celle de la session. Le but est de réduire le nombre de requêtes SQL d'une même transaction.
  • le cache de niveau 2 ou cache de second niveau : il n'est pas actif par défaut. Il est implémenté dans la SessionFactory. La visibilité de ce cache est l'application. Une fois activé, sa durée de vie est celle de l'instance de type SessionFactory. Le rôle principal du cache de second niveau est de partager des données entre sessions.

L'utilisation du cache de premier niveau est obligatoire puisqu'il est implémenté dans l'objet de type Session : toutes les requêtes permettant l'obtention ou la mise à jour des données passent par la session. Une fois que la session est fermée, le contenu du cache de premier niveau est effacé.

Pour réduire les échanges entre l'application et la base de données, le cache de second niveau conserve les données lues de la base de données pour les partager entre les sessions. Ces données stockées au niveau de la SessionFactory sont donc accessibles par toutes les sessions, évitant ainsi l'exécution de requêtes SQL si les résultats de celles-ci sont déjà dans le cache.

Le cache de second niveau est optionnel. Le cache de premier niveau est toujours consulté avant de consulter de cache de second niveau.

Le cache de second niveau peut être utilisé pour stocker trois types d'éléments correspondant à trois cas d'utilisations du cache:

  • les entités (class cache)
  • les associations de type many (collection cache)
  • le résultat des requêtes (query cache)

L'implémentation du cache de second niveau requiert l'utilisation d'une solution de cache d'un tiers. Cette solution doit proposer une classe qui implémente l'interface org.hibernate.cache.CacheProvider.

La configuration du cache de second niveau requiert plusieurs étapes :

  • définir les stratégies transactionnelles qui devront être utilisées par le cache de second niveau pour chaque entité et association
  • choisir une implémentation du cache qui supporte la ou les stratégies requises en plus des critères de choix plus généraux (fiabilité, performance, fonctionnalités, documentation, ...)
  • configurer Hibernate pour utiliser le cache
  • configurer de manière spécifique le cache

Le cache des requêtes permet de conserver le résultat de l'exécution des requêtes. Ce cache utilise la requête avec ses paramètres comme clé à laquelle il associe uniquement les identifiants et les types des entités qui sont obtenus lors de l'exécution de la requête. Il repose sur l'utilisation du cache de second niveau pour obtenir les données des entités concernées.

L'utilisation du cache des requêtes est optionnelle : il n'est pas activé par défaut. Il requiert la définition de deux régions particulières dans le cache de second niveau :

  • la première stocke les résultats (identifiants et types) des requêtes
  • la seconde stocke le timestamp de dernière mise à jour de chaque table

Ce cache est essentiellement utile pour les requêtes fréquemment exécutées avec les mêmes valeurs de paramètres.

 

56.12.2.1. La base des exemples de cette section

Les exemples de cette section utilisent la version 4.1 d'Hibernate.

Cette section va utiliser une petite application qui accède à une base de données composées de deux tables : pays et devise. Le besoin fonctionnel précise qu'l existe une relation 1-N entre devise et pays partant du principe qu'un pays possède une devise et qu'une devise peut être utilisée par plusieurs pays.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate
Mapping DTD//EN" "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.jmdoudoux.test.hibernatecache.entity.Pays"
       table="pays" >
        <id name="id" column="id" type="int" >
            <generator class="identity" />
        </id>
        <property name="codeIso" column="code_iso"
           not-null="true" type="string" />
        <property name="nom" not-null="true" type="string" />
        <many-to-one name="devise" column="FK_DEVISE"
           class="com.jmdoudoux.test.hibernatecache.entity.Devise" />
    </class>
</hibernate-mapping>

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC  "-//Hibernate/Hibernate Mapping
DTD//EN" "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.jmdoudoux.test.hibernatecache.entity.Devise"
      table="DEVISE" >
        <id name="id" column="ID" >
            <generator class="identity" />
        </id>
        <property name="code" column="CODE" />
        <property name="libelle" column="LIBELLE" />
        <set name="pays" >
           <key column="FK_DEVISE" />
           <one-to-many class="com.jmdoudoux.test.hibernatecache.entity.Pays"/>
        </set>
    </class>
</hibernate-mapping>

Dans le fichier de configuration d'Hibernate, le cache de second niveau est désactivé et l'affichage des requêtes SQL exécutées est demandé.

Résultat :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mabdd</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password"></property>
    <property name="hibernate.connection.pool_size">1</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Cache de second niveau désactivé  -->
    <property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property>
    <!-- Afficher les requêtes SQL exécutées sur la sortie standard -->
    <property name="hibernate.show_sql">true</property>
    <mapping resource="Pays.hbm.xml" />
    <mapping resource="Devise.hbm.xml" />
  </session-factory>
</hibernate-configuration>

 

56.12.2.2. Le cache de premier niveau

Le cache de premier niveau est implémenté dans la classe Session. Hibernate l'utilise par défaut pour maintenir l'état des entités lues et modifiées dans la transaction courante.

Une entité est stockée dans le cache de la session, c'est à-dire le cache de premier niveau, à chaque fois :

  • que les méthodes load() ou get() sont invoquées
  • que les méthodes save(), update() ou saveOrUpdate() sont invoquées

Le cache de premier niveau est activé par défaut et il n'est pas possible de le désactiver.

Le cache de premier niveau permet à Hibernate de conserver les données lues et celles qui sont modifiées : ceci permet de limiter le nombre de requêtes exécutées pour les lectures et les mises à jour sur la base de données.

Grâce au cache de premier niveau, sur une même session, plusieurs invocations de la méthode get() qui retourneraient la même entité ne nécessiteront qu'une seule requête SQL lors de la première invocation. Pour les invocations suivantes, les données seront obtenues directement du cache sans relecture dans la base de données.

Exemple :
  public static void lirePaysDeuxFois() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      Pays pays = (Pays) session.load(Pays.class, new Integer(4));
      System.out.println("pays : id=" + pays.getId() + " codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      pays = (Pays) session.load(Pays.class, new Integer(4));
      System.out.println("pays : id=" + pays.getId() + " codeIso=" 
          + pays.getCodeIso() + " nom=" + pays.getNom());
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }

Résultat :
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
pays : id=4 codeIso=LU
nom=Luxembourg
pays : id=4 codeIso=LU
nom=Luxembourg

Bien que la méthode load() soit invoquée deux fois, l'entité n'est lue qu'une seule fois dans la base de données et est stockée dans le cache de premier niveau. Lors de la seconde invocation de la méthode load(), l'entité est obtenue du cache de premier niveau évitant ainsi une seconde lecture dans la base de données.

Hibernate utilise toujours de manière transparente le cache de premier niveau.

L'interface Session propose plusieurs méthodes pour gérer le contenu du cache de premier niveau.

Lorsque la méthode flush() de la session est invoquée, l'état des entités contenues dans la session est synchronisé dans la base de données. Ces entités restent dans la session jusqu'à sa fermeture.

La méthode contains() renvoie un booléen qui précise si une instance d'une entité est associée à la session.

La méthode clear() permet de supprimer toutes les entités contenues dans la session.

La méthode evict() permet de retirer une entité de la session et les entités qui lui sont associées si l'attribut cascade de l'association à pour valeur all ou all-delete-orphan. Cette méthode est particulièrement pratique lorsque des requêtes retournent de nombreuses entités et que ces dernières peuvent être retirées du cache car devenues inutiles.

 

56.12.2.3. Le cache de second niveau

Dès que l'on utilise le cache de second niveau, il ne faut pas oublier que le cache de premier niveau est toujours actif. Hibernate tente de trouver une entité dans le cache de premier niveau, si elle n'est pas trouvée, Hibernate tente de la trouver dans le cache de second niveau. Si elle n'est toujours pas trouvée alors elle est lue de la base de données.

L'utilisation du cache de premier niveau est transparente car complètement prise en charge par Hibernate. L'utilisation du cache de second niveau est plus complexe : les données contenues dans le cache sont partagées entre les transactions et peuvent même être partagées entre plusieurs JVM.

Le cache de second niveau est partagé entre toutes les sessions alors que la portée du cache de premier niveau est au niveau d'une seule session. Hibernate propose une API pour permettre de choisir l'implémentation du cache qui sera utilisée pour le cache de second niveau.

Le cache de second niveau est implémenté dans un objet de type SessionFactory : généralement une application ne possède qu'une seule instance de ce type d'objet. La durée de vie du cache de second niveau est donc liée à la durée de vie de l'instance de type SessionFactory. L'accès à cet objet est commun à toutes les sessions. Le contenu du cache de second niveau peut donc être partagé entre plusieurs sessions.

La portée du cache de second niveau est la JVM ou un cluster si l'implémentation du cache supporte l'utilisation dans ce cadre.

Le cache de second niveau propose un mécanisme puissant qui peut permettre, dans des cas précis, d'améliorer les performances d'une application. Cependant, l'utilisation du cache de second niveau introduit une complexité supplémentaire : par exemple, l'utilisation de caches augmente toujours le risque d'obtenir des données inconsistantes.

Les données en lecture seule dans le cache sont faciles à utiliser et à gérer. Dans le cache, la gestion et l'utilisation des données pouvant être mises à jour est plus délicate.

L'usage du cache de second niveau le rend surtout intéressant pour des données en lecture seule. Des données contenues dans le cache pouvant être mises à jour risque de ne pas être fraîches. Il est alors très important de bien définir la politique d'éviction des entrées dans le cache, surtout si la fraîcheur des données est importante.

Les objets lus de la base de données peuvent être stockés dans le cache de second niveau et ainsi être partagés par les sessions pour éviter leur relecture par les autres sessions. Bien sûr ce partage est facile si les données sont uniquement lues et jamais modifiées. Ce partage est plus complexe lorsque les données peuvent être mises à jour par une session.

Le cache de second niveau ne peut jamais être informé d'une modification réalisée sans utiliser Hibernate. C'est le cas notamment si ces modifications sont réalisées par d'autres applications, des procédures stockées, des triggers, ... Dans ce cas, il faut utiliser des mécanismes qui permettent de supprimer une entité du cache ou le contenu d'une région ou prévoir des suppressions périodiques du contenu du cache.

Hibernate impose une utilisation sélective du cache de second niveau car il ne peut pas être appliqué sur toutes les opérations. Le cache de second niveau ne s'utilise que sur trois types d'éléments :

  • les données des entités
  • les associations de type many
  • le résultat des requêtes

Par défaut, le cache de second niveau n'est pas activé : pour l'utiliser, il est nécessaire de l'activer et de le configurer. Une fois le cache de second niveau activé, seuls les éléments configurés (entités, associations, requêtes) pourront y être stockés.

Le cache de premier niveau stocke directement les instances des entités. Le cache de second niveau, lui, stocke les entités sous une forme «sérialisée». Les entités ne sont pas stockées sous la forme d'instance : Hibernate stocke les attributs de l'entité sous une forme nommée état déshydraté (dehydrated state). Lorsqu'une entité est obtenue du cache, une nouvelle instance est créée à partir des informations stockées dans le cache. La clé dans le cache est l'identifiant de l'entité et la valeur est la forme déshydratée de l'entité.

Il est très important de comprendre le fonctionnement du cache de second niveau pour en tirer le meilleur parti.

 

56.12.3. La configuration du cache de second niveau

Par défaut, le cache de second niveau n'est pas activé. Son activation se fait en modifiant la configuration d'Hibernate.

La propriété hibernate.cache.use_second_level permet d'activer le cache de second niveau en lui affectant la valeur true.

Hibernate propose une solution de type plug-in pour lui permettre d'utiliser une implémentation d'un cache tiers comme cache de second niveau.

Hibernate offre une implémentation reposant sur un objet de type HashTable mais son utilisation n'est pas recommandée car elle ne propose pas les fonctionnalités minimum d'un cache (gestion de sa taille, de la durée de vie des éléments contenus, ...)

La plupart des principaux caches open source peuvent s'utiliser avec Hibernate moyennant un peu de configuration. Une partie de cette configuration est faite dans le fichier de configuration d'Hibernate en utilisant plusieurs propriétés.

La propriété hibernate.cache.provider_class permet de préciser le nom pleinement qualifié de la classe du fournisseur du cache jusqu'à la version 3.2 d'Hibernate.

Exemple pour ne préciser aucun fournisseur

Exemple :
    <property name="hibernate.cache.provider_class">
org.hibernate.cache.internal.NoCacheProvider</property>

A partir de la version 3.3, c'est la propriété hibernate.cache.region.factory_class qui doit être utilisée.

Exemple pour ne préciser aucun fournisseur

Exemple :
    <property name="hibernate.cache.region.factory_class">
org.hibernate.cache.internal.NoCachingRegionFactory</property>

La propriété hibernate.cache.use_structured_entries à la valeur true permet de demander à Hibernate de stocker les données dans le cache de manière lisible.

La propriété hibernate.cache.use_minimal_puts permet de demander à Hibernate de limiter les écritures dans le cache, ce qui entrainera plus de lectures dans la base de données. La valeur true est intéressante pour les caches en cluster.

La propriété hibernate.cache.use.region_prefix permet de préciser un préfixe qui sera utilisé pour le nom de chaque région du cache de second niveau.

La propriété hibernate.cache.defaut_cache_concurrency_strategy permet de préciser la stratégie d'usage transactionnel utilisée par l'annotation @Cache. Il est possible de remplacer cette stratégie définie par défaut en utilisant l'attribut strategy de l'annotation @Cache.

 

56.12.3.1. Les différents caches supportés par Hibernate

Hibernate fournit en standard une implémentation du cache utilisant des objets de type HashTable : c'est une solution minimaliste qu'il n'est pas recommandé d'utiliser en production.

Hibernate propose le support en standard de plusieurs solutions open source comme implémentation du cache de second niveau :

  • Terracotta Ehcache (Easy Hibernate Cache)
  • OSCache (Open Symphony Cache)
  • Swarm Cache
  • JBoss Tree Cache : cache utilisable en cluster qui nécessite un gestionnaire de transactions

Chacun de ces caches possède des caractéristiques et des fonctionnalités :

  • Ehcache : léger, rapide, facile à configurer et à utiliser, supporte les stratégies read-only, nonstrict-read-only et read-write, cache mémoire avec débordement sur disque, support du cluster
  • OSCache : supporte les stratégies read-only et read-write, cache mémoire avec débordement sur disque, support du cluster en utilisant JGroups ou JMS
  • SwarmCache : supporte les stratégies read-only et nonstrict-read-write, support du cluster en utilisant JGroups, approprié pour des applications qui réalisent plus de lectures que d'écritures
  • JBoss TreeCache : riche en fonctionnalités et répliqué, supporte la stratégie transactional, utilisation de la classe org.hibernate.cache.TreeCacheProvider (pour la version 1) et org.hibernate.cache.jbc.JbossCacheRegionFactory (pour la version 2)

Avant la version 3.2, Hibernate utilisait par défaut Ehcache. A partir de la version 3.2 d'Hibernate, il n'y a plus d'implémentation par défaut : celle à utiliser doit être explicitement précisée dans la configuration d'Hibernate.

Hibernate impose de n'utiliser qu'une seule implémentation pour le cache de second niveau : il faut donc la choisir judicieusement en fonction des besoins.

Cache

Type

Support en Cluster

Cache de requêtes supporté

Hashtable (ne pas utiliser en production)

Mémoire

 

Oui

Ehcache

Mémoire, disque

Oui

Oui

OSCache

Mémoire, disque

 

Oui

SwarmCache

En cluster (multicast ip)

Oui (invalidation de cluster)

 

JBoss TreeCache

En cluster (multicast ip), transactionnel

Oui (replication)

Oui

 

56.12.3.2. La configuration du cache de second niveau

Une implémentation du cache doit fournir une classe qui implémente l'interface org.hibernate.cache.CacheProvider (jusqu'à la version 3.2) ou l'interface org.hibernate.RegionFactory (à partir de la version 3.3).

Il est nécessaire de préciser la classe du fournisseur qui doit être utilisée en donnant son nom pleinement qualifié comme valeur d'une propriété dans le fichier de configuration d'Hibernate.

La configuration du cache de second niveau est différente selon la version d'Hibernate utilisée :

  • Jusqu'à la version 3.2, il faut utiliser la propriété hibernate.cache.provider_class
  • A partir de la version 3.3, il faut utiliser la propriété hibernate.cache.region.factory_class

L'exemple ci-dessous permet d'utiliser le cache Ehcache avec Hibernate 3.2.

Exemple :
<property name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider</property>

La configuration du cache dépend de l'implémentation utilisée.

Par défaut, la région utilisée est celle définie par défaut dans le cache. Il est aussi possible de définir et configurer des régions dédiées.

Hibernate utilise des conventions de nommage particulières pour les noms des régions du cache :

  • pour les entités : c'est le nom pleinement qualifié de la classe de l'entité
  • pour les associations : c'est le nom pleinement qualifié de la classe de l'entité, suivi d'un caractère point, suivi du nom du champ de la collection
  • pour les requêtes : Hibernate utilise par défaut deux régions (StandardQueryCache et UpdateTimestampsCache).

Le nom de ces deux régions est différent selon la version d'Hibernate utilisée :

  • jusqu'à Hibernate version 3.1 : net.sf.hibernate.cache.StandardQueryCache et net.sf.hibernate.cache.UpdateTimestampsCache
  • à partir de la version 3.2 : org.hibernate.cache.StandardQueryCache et org.hibernate.cache.UpdateTimestampsCache

 

56.12.3.3. La configuration du cache Ehcache

La version d'Ehcache utilisée dans cette section est la version 2.4.3.

Dans le fichier de configuration d'Hibernate, il faut activer l'utilisation du cache de second niveau avec Ehcache en donnant à la valeur de la propriété hibernate.cache.region.factory_class le nom pleinement qualifié d'une classe qui hérite de AbstractEhcacheRegionFactory.

Deux implémentations sont fournies par Hibernate dans la bibliothèque hibernate-ehcache-xxx.jar :

  • org.hibernate.cache.ehcache.EhCacheRegionFactory
  • org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory : cette implémentation est à utiliser lorsqu'une seule configuration est requise. Il ne faut pas l'utiliser si plusieurs instances d'Hibernate sont requises.
Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- ... -->
    <!-- Cache de second niveau activé avec ehcache  -->
    <!-- property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property-->
    <property name="hibernate.cache.region.factory_class">
      org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
    <!-- ... -->
  </session-factory>
</hibernate-configuration>

EhCache fournit aussi sa propre implémentation qui est différente selon la version d'Hibernate utilisée.

Jusqu'à la version 3.2 incluse, il faut utiliser la classe net.sf.ehcache.hibernate.EhCacheProvider ou la classe net.sf.ehcache.hibernate.SingletonEhCacheProvider comme valeur de la propriété hibernate.cache.provider_class.

A partir de la version 3.3, il faut utiliser la classe net.sf.ehcache.hibernate.EhCacheRegionFactory ou la classe net.sf.ehcache.hibernate.SingletonEhCacheRegionFactory comme valeur de la propriété hibernate.cache.region.factory_class.

La configuration d'EhCache se fait dans un fichier XML nommé par défaut ehcache.xml qui doit être dans le classpath. Cette configuration porte sur EhCache lui-même, la région par défaut et éventuellement chaque région utilisée.

Le tag racine de ce document XML est <ehcache>.

Le tag <diskStore> permet de configurer le stockage sur disque des données du cache.

L'attribut path permet de préciser le chemin du répertoire dans le lequel EhCache va stocker les données du cache sur disque.

EhCache créé selon la configuration des fichiers pour chaque cache concerné avec l'extension .index et .data.

Le tag <defaultCache> permet de configurer le cache par défaut.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
  <diskStore path="C:\temp\cache" />
  <defaultCache maxElementsInMemory="100" eternal="false"
    timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" />
</ehcache>

La propriété net.sf.ehcache.configurationResourceName du fichier de configuration permet de préciser le nom du chemin du fichier de configuration d'EhCache.

Il est nécessaire de configurer la région par défaut et éventuellement des régions dédiées.

Par défaut, Hibernate va rechercher et utiliser une région dont le nom est le nom pleinement qualifié de l'entité qui doit être mise en cache. Si elle n'est pas trouvée, alors la région par défaut sera utilisée.

Résultat :
WARN    
[main]:org.hibernate.cache.ehcache.AbstractEhcacheRegionFactory -
HHH020003: Could not find a specific ehcache configuration for cache named
[com.jmdoudoux.test.hibernatecache.entity.Pays]; using defaults.

La configuration d'une région se fait en utilisant le tag <cache>.

La configuration peut se faire en utilisant plusieurs attributs :

Attribut

Rôle

name

Le nom du cache. Il doit être unique et sera utilisé par Hibernate comme la désignation d'une région

maxElementsInMemory

Préciser le nombre d'éléments maximum que peut contenir la région en mémoire. Si le maximum est atteint et qu'un nouvel élément doit être inséré, alors il y a une éviction d'un élément selon la configuration. La valeur par défaut est 0, indiquant un nombre infini

timeToIdleSeconds

Durée de vie en secondes des objets inaccédés (TTI). La valeur par défaut est 0, indiquant un temps infini

timeToLiveSeconds

Durée de vie en secondes des objets dans le cache quelque soit leur utilisation ou non (TTL) . La valeur par défaut est 0, indiquant un temps infini

eternal

true ou false. La valeur true permet de préciser que les données contenues dans la région ne peuvent pas être retirées. Ce paramètre est prioritaire sur les TTI et TTL si sa valeur est true

overflowToDisk

true ou false : écriture d'éléments sur le système de fichiers si la région contient trop d'éléments

diskPersistent

true ou false. La valeur permet de demander de conserver les données écrites sur le système de fichiers lorsque la JVM est redémarrée. La valeur par défaut est false

diskExpiryThreadIntervalSeconds

Préciser l'intervalle en secondes entre chaque exécution de la règle d'éviction des données du cache sur disque. La valeur par défaut est 120.

memoryStoreEvictionPolicy

Règle à appliquer pour l'éviction de données lorsque la taille maximale du cache est atteinte. Les règles possibles sont :

  • LRU (Least Recently Used)
  • FIFO (First In First Out)
  • LFU (Least Frequently Used)

maxElementsOnDisk

Préciser le nombre maximum d'éléments du cache sur disque. La valeur par défaut est 0, indiquant un nombre illimité

diskSpoolBufferSizeMB

Préciser la taille d'un tampon qui est utilisé pour stocker temporairement les données à écrire de manière asynchrone sur disque. Chaque cache possède son propre tampon. La taille par défaut est 30Mo.


Il est important de bien tenir compte dans la configuration de l'expiration des données du cache.

Le paramètre TTI permet notamment de retirer du cache des éléments qui sont peu fréquemment utilisés et ainsi faire de la place aux nouveux éléments.

Le paramètre TTL permet de rafraîchir périodiquement des données en forçant leur éviction de manière répétée.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
  <diskStore path="C:\temp\cache" />
  <defaultCache maxElementsInMemory ="100" eternal="false"
    timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" />
  <cache name="com.jmdoudoux.test.hibernatecache.entity.Pays"
    maxElementsInMemory="250" eternal="true" overflowToDisk="false" />
  <cache name="com.jmdoudoux.test.hibernatecache.entity.Section"
    maxElementsInMemory="100" eternal="false" timeToIdleSeconds="300"
    timeToLiveSeconds="600" overflowToDisk="true" />
</ehcache>

Un message est affiché dans le journal pour chaque entité configurée dans le cache EhCache.

Résultat :
WARN    
[main]:org.hibernate.cache.ehcache.internal.strategy.EhcacheAccessStrategyFactoryImpl
- HHH020007: read-only cache configured for mutable entity
[com.jmdoudoux.test.hibernatecache.entity.Pays]

Pour le cache des relations, par défaut l'implémentation d'EhCache utilise une région qui se nomme du nom pleinement qualifié de l'entité suivi du caractère point et du nom de l'attribut de la relation.

Résultat :
WARN    
[main]:org.hibernate.cache.ehcache.AbstractEhcacheRegionFactory -
HHH020003: Could not find a specific ehcache configuration for cache named
[com.jmdoudoux.test.hibernatecache.entity.Devise.pays]; using defaults.

Dans l'exemple ci-dessus, la région du cache concernant la relation 1-N entre devise et pays n'est pas configurée. Il suffit alors de définir une nouvelle région dans la configuration d'EhCache pour éviter l'utilisation de la configuration par défaut.

Pour le cache des requêtes, par défaut Hibernate utilise deux régions qui se nomment :

  • org.hibernate.cache.spi.UpdateTimestampsCache 
  • org.hibernate.cache.internal.StandardQueryCache 
Résultat :
INFO     [main]:org.hibernate.cache.spi.UpdateTimestampsCache
- HHH000250: Starting update timestamps cache at region:
org.hibernate.cache.spi.UpdateTimestampsCache
WARN    
[main]:org.hibernate.cache.ehcache.AbstractEhcacheRegionFactory -
HHH020003: Could not find a specific ehcache configuration for cache named
[org.hibernate.cache.spi.UpdateTimestampsCache]; using defaults.
INFO    
[main]:org.hibernate.cache.internal.StandardQueryCache - HHH000248:
Starting query cache at region: org.hibernate.cache.internal.StandardQueryCache
WARN    
[main]:org.hibernate.cache.ehcache.AbstractEhcacheRegionFactory -
HHH020003: Could not find a specific ehcache configuration for cache named
[org.hibernate.cache.internal.StandardQueryCache]; using defaults.

Dans l'exemple ci-dessus, les deux régions du cache concernant les requêtes ne sont pas configurées. Il suffit alors de définir deux nouvelles régions dans la configuration d'EhCache pour éviter l'utilisation de la configuration par défaut.

Le nom de la région utilisée par Hibernate pour le cache des résultats des requêtes est org.hibernate.cache.StandardQueryCache.

Exemple :
<cache name="org.hibernate.cache.StandardQueryCache"
maxEntriesLocalHeap="50" eternal="false" timeToLiveSeconds="300"
overflowToDisk="true"/>

Le cache des requêtes utilise aussi une autre région dont le nom est org.hibernate.cache.UpdateTimestampsCache. Cette région contient la date/heure de dernière mise à jour de chaque table. Pour le bon fonctionnement du cache des requêtes, il est préférable que cette région ne soit jamais invalidée.

Exemple :
<cache name="org.hibernate.cache.UpdateTimestampsCache"
maxEntriesLocalHeap="500" eternal="true" overflowToDisk="true"/>

Il est possible de définir et d'utiliser une région dédiée pour une ou plusieurs requêtes. Le nom attribué dans la configuration de la région dans le fichier ehcache.xml doit être utilisé comme paramètre de la méthode setCacheRegion() pour les requêtes (Query ou Criteria) qui doivent être mises en cache. La valeur de l'attribut name n'est pas imposée mais par convention elle commence par "query." et ce nom doit être unique pour toutes les régions.

Exemple :
<cache name="query.paysPourUneMonnaie" maxEntriesLocalHeap="20"
eternal="false" timeToLiveSeconds="3600" overflowToDisk="true"/>

Enfin, il faut ajouter les bibliothèques requises pour EhCache dans le classpath. Celles-ci peuvent être trouvées dans le sous-répertoire lib/optional/ehcache.

 

56.12.4. Les stratégies d'usage transactionnel du cache

Une stratégie d'usage transactionnel permet de déterminer comment les éléments vont être obtenus et stockés dans le cache de second niveau et comment les accès concurrents vont être gérés.

Hibernate propose le support de quatre stratégies transactionnelles pour le cache de second niveau :

  • read-only : utilisable pour des entités qui ne sont jamais mises à jour.
  • read-write : utilisable pour des entités qui sont mises à jour occasionnellement en utilisant la sémantique du niveau d'isolation read committed. Cette stratégie impose un léger surcoût lors de son utilisation.
  • nonstrict-read-write : utilisable pour des entités qui sont mises à jour occasionnellement. L'entité dans le cache n'est jamais verrouillée. Si un accès concurrent à une entité est fait, cette stratégie ne garantit pas que ce qui est retourné du cache soit l'image des données correspondantes dans la base de données. Elle ne propose donc aucune gestion des accès concurrents.
  • transactional : utilisable uniquement dans un environnement possédant un gestionnaire de transactions distribuées respectant l'API JTA qui permet d'utiliser la sémantique du niveau d'isolation repeatable read.

Une stratégie d'usage transactionnel doit être précisée pour chaque entité et collection configurées pour être mises dans le cache de second niveau.

 

56.12.4.1. La stratégie read-only

La stratégie read-only est utilisable sur des données qui ne seront utilisées qu'en lecture seule et ne seront donc jamais mises à jour. Elle convient parfaitement à des entités de type données de référence.

La mise en oeuvre de cette stratégie est la plus simple et la plus performante, de plus elle est utilisable en cluster.

Il est préférable d'utiliser la stratégie read_only sur des entités dont l'attribut mutable possède la valeur false.

Remarque : l'utilisation de la stratégie read-only interdit les mises à jour sur une entité mais elle n'interdit pas l'ajout de nouvelles entités.

 

56.12.4.2. La stratégie read-write

La stratégie read-write est utilisable sur des données qui pourront être lues et/ou modifiées sans requérir un niveau d'isolation transactionnelle de type Serializable.

Les mises à jour des entités mises en cache sont aussi faites dans le cache. Ces mises à jour sont faites de manière concurrente grâce à la pose d'un verrou. Cette stratégie est similaire au niveau d'isolation read committed.

Avec la stratégie read-write, dès qu'une entité est mise à jour, un verrou est posé pour ses données dans le cache empêchant leur accès par d'autres sessions qui sont alors obligées de relire les données dans la base de données. Lorsque la transaction est validée, les données du cache sont rafraichies et le verrou est retiré.

Cette stratégie permet d'éviter d'avoir des dirty read et permet aux sessions d'obtenir des données de manière read committed aussi bien de la base de données que du cache.

Cette stratégie induit un léger surcoût lié au verrouillage des données dans le cache.

Il est important que la transaction soit terminée avant l'invocation de la méthode close() ou de la méthode disconnect() de la session.

Cette stratégie ne doit pas être utilisée si le niveau d'isolation des transactions requis est Serializable.

Pour pouvoir être utilisé dans un cluster, l'implémentation du cache doit permettre de poser les verrous dans les différents noeuds du cluster. C'est par exemple le cas du cache Coherence d'Oracle.

 

56.12.4.3. La stratégie nonstrict-read-write

La stratégie nonstrict_read_write est utilisable pour des données qui ne changent pas fréquemment voire même jamais. Elle ne vérifie pas que deux transactions mettent à jour des données qui sont dans le cache : aucun verrou n'est posé lors de ces modifications. Les accès concurrents sont donc possibles mais cette stratégie ne garantit pas que les données retournées seront les plus fraiches et donc le reflet de ce qui est dans la base de données.

Si l'application peut accepter que les données ne soient pas toujours consistantes et que les données ne soient fréquemment modifiées, l'utilisation de la stratégie nonstrict-read-write à la place de la stratégie read-write peut améliorer les performances.

La stratégie nonstrict-read-write retire les données du cache d'une entité mise à jour lorsque la méthode flush() de la session est invoquée.

La stratégie nonstrict-read-write ne pose jamais de verrou. Lorsque qu'un objet doit être modifié, les anciennes valeurs restent dans le cache jusqu'à ce que la transaction soit validée par un commit. Si une autre session veut accéder à l'objet, elle obtiendra les données du cache (dirty read). Dès que la transaction est validée, les données de l'entité sont supprimées du cache : lorsqu'une session voudra obtenir les données de l'entité, elle sera donc forcer de relire les données de la base de données et de les insérer dans le cache.

La stratégie nonstrict-read-write est donc utilisable si l'application peut supporter des dirty reads qui peuvent arriver lorsque les données sont répercutées dans la base de données sans être encore retirées du cache.

Avec la stratégie nonstrict-read-write, il est préférable de configurer l'invalidation périodique de la région afin de permettre d'améliorer la fraîcheur des données.

 

56.12.4.4. La stratégie transactional

La stratégie transactional doit être utilisée dans un environnement utilisant un gestionnaire de transactions distribuées de type JTA.

Cette stratégie n'est utilisable qu'avec un cache transactionnel comme JBoss TreeCache.

Avec les stratégies nonstrict-read-write et read-write, le cache est mis à jour de manière asynchrone une fois que la transaction est validée. Avec la stratégie transactional, le cache est mis à jour en même temps que la transaction est validée.

Il faut préciser à Hibernate le nom d'une classe qui sera instanciée pour lui permettre d'obtenir le gestionnaire de transactions du conteneur dans lequel l'application s'exécute. La valeur de cette propriété est donc dépendante de l'environnement d'exécution.

Comme l'accès au TransactionManager de JTA n'est pas standardisé, cette classe permet d'obtenir l'instance de l'environnement d'exécution correspondant.

Jusqu'à la version 3.6 incluse, la classe à utiliser doit implémenter l'interface TransactionManagerLookup. Il faut fournir son nom pleinement qualifié à la propriété hibernate.transaction.manager_lookup_class.

Conteneur

Propriété hibernate.transaction.manager_lookup_class

Oracle OAS

org.hibernate.transaction.OC4JtransactionManagerLookup

JBoss AS

org.hibernate.transaction.JbossTransactionManagerLookup

JBoss Transactions

org.hibernate.transaction.JbossTSStandaloneTransactionManagerLookup

GlassFish

org.hibernate.transaction.SunONETransactionManagerLookup

JOTM

org.hibernate.transaction.JOTMTransactionManagerLookup

IBM Websphere AS 4 à 5.1

org.hibernate.transaction.WebSphereTransactionManagerLookup

IBM Websphere AS 6 et supérieur

org.transaction.WebSphereExtendedJTATransactionLookup

Atomikos

com.atomikos.icatch.jta.hibernate3.TransactionManagerLookup

Resin

org.hibernate.transaction.ResinTransactionManagerLookup

Orion

org.hibernate.transaction.OrionTransactionManagerLookup

Oracle Weblogic

org.hibernate.transaction.WeblogicTransactionManagerLookup

Infinispan

org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup

Apache Tomee

org.apache.openejb.hibernate.TransactionManagerLookup

JonAs

org.hibernate.transaction.JOnASTransactionManagerLookup

Jrun4

org.hibernate.transaction.JRun4TransactionManagerLookup


A partir de la version 4.0 d'Hibernate, il faut utiliser le service de type JtaPlatform.

Il faut utiliser la propriété hibernate.transaction.jta.platform et lui passer en paramètre le nom pleinement qualifié d'une classe qui implémente l'interface org.hibernate.service.jta.platform.spi.JtaPlatform.

Conteneur

Propriété hibernate.transaction.jta.platform

Bitronix

org.hibernate.service.jta.platform.internal.BitronixJtaPlatform

Borland Enterprise Server

org.hibernate.service.jta.platform.internal.BorlandEnterpriseServerJtaPlatform

JBoss AS

org.hibernate.service.jta.platform.internal.JBossAppServerJtaPlatform

JBoss TM

org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform

JonAs

org.hibernate.service.jta.platform.internal.JOnASJtaPlatform

JOTM

org.hibernate.service.jta.platform.internal.JOTMJtaPlatform

JRun 4 AS

org.hibernate.service.jta.platform.internal.JRun4JtaPlatform

Aucun

org.hibernate.service.jta.platform.internal.NoJtaPlatform

Oracle OC4J

org.hibernate.service.jta.platform.internal.OC4JJtaPlatform

Orion AS

org.hibernate.service.jta.platform.internal.OrionJtaPlatform

Resin AS

org.hibernate.service.jta.platform.internal.ResinJtaPlatform

Oracle Glassfish

org.hibernate.service.jta.platform.internal.SunOneJtaPlatform

Pont vers TransactionManagerLookup

org.hibernate.service.jta.platform.internal.TransactionManagerLookupBridge

Oracle Weblogic

org.hibernate.service.jta.platform.internal.WeblogicJtaPlatform

IBM Websphere 6 et supérieur

org.hibernate.service.jta.platform.internal.WebSphereExtendedJtaPlatform

IBM Websphere 4 à 5.1

org.hibernate.service.jta.platform.internal.WebSphereJtaPlatform


Exemple :
<property name="hibernate.transaction.jta.platform"
value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform" /> 

L'utilisation de l'API TransactionManagerLookup affiche une entrée de type warning dans le journal.

Résultat :
HHH000427:
Using deprecated org.hibernate.transaction.TransactionManagerLookup strategy
[hibernate.transaction.manager_lookup_class], use newer
org.hibernate.service.jta.platform.spi.JtaPlatform strategy instead
[hibernate.transaction.jta.platform]

 

56.12.4.5. Le support des stratégies par les différents caches

Les caches supportés en standard par Hibernate proposent le support d'une ou plusieurs stratégies.

Cache

read-only
(lecture seule)

nonstrict-read-write
(lecture-écriture non stricte)

read-write
(lecture-écriture)

transactional
(transactionnel)

Hashtable (ne pas utiliser en production)

oui

oui

oui

 

EHCache

oui

oui

oui

oui (depuis la version 2.1 d'EhCache)

OSCache

oui

oui

oui

 

SwarmCache

oui

oui

 
 

JBoss TreeCache 1.x

oui

oui

 
 

JBoss TreeCache 2

oui

 
 

oui

 

56.12.5. Le cache des entités

Le cache des entités est utilisé lors de la lecture d'entités de la base de données à partir de leur identifiant (par exemple en utilisant les méthodes Session.get() ou Session.load()) et lors du parcours des relations de type OneToOne ou ManyToOne.

Par défaut, Hibernate utilise pour une entité une région dont le nom est le nom pleinement qualifié de la classe de l'entité. Si aucune région possédant ce nom n'est définie alors c'est la région par défaut qui est utilisée.

Hibernate ne stocke pas directement les instances des entités lues de la base de données dans le cache mais une copie sérialisée : ceci permet d'éviter des problèmes d'accès concurrents si plusieurs transactions font référence au même objet.

Les identifiants des entités sont utilisés comme index dans le cache.

Exemple :
package com.jmdoudoux.test.hibernatecache;

import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import com.jmdoudoux.test.hibernatecache.entity.Devise;
import com.jmdoudoux.test.hibernatecache.entity.Pays;

public class TestHibernateCache {
  private static SessionFactory sessionFactory = null;
  public static void main(String[] args) {
    try {
      sessionFactory = new Configuration().configure().buildSessionFactory();
      for (int i = 0; i < 3; i++) {
        lirePays();
      }
    } catch (Throwable ex){
      System.err.println("Erreur durant les traitements" + ex);
    } finally {
      sessionFactory.close();
    }
  }

  public static void lirePays() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      Pays pays = (Pays) session.load(Pays.class, new Integer(4));
      System.out.println("pays : id=" + pays.getId() + "codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }  
}

Résultat :
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=4 codeIso=LU nom=Luxembourg
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=4 codeIso=LU nom=Luxembourg
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=4 codeIso=LU nom=Luxembourg

Par défaut, aucune entité n'est mise dans le cache de second niveau qui a été activé et configuré.

Chaque entité concernée doit être configurée pour être stockée dans le cache de second niveau. Cette configuration doit permettre de préciser la stratégie de gestion des accès concurrents aux données des entités.

Il existe plusieurs manières de déclarer qu'une entité doit être mise dans le cache de second niveau :

  • utiliser le tag <cache> dans le fichier de mapping hbm.xml
  • utiliser l'annotation @Cache sur la classe de l'entité
  • utiliser le tag <class-cache> dans le fichier de configuration d'Hibernate

Dans le fichier de mapping, la définition de la mise en cache pour une entité se fait en utilisant le tag <cache>.

Exemple :
<cache usage="transactional|read-write|nonstrict-read-write|read-only"
region="nom_de_la_region" include="all|non-lazy" />

L'attribut obligatoire usage permet de préciser la stratégie de concurrence d'accès transactionnel qui sera appliquée avec l'entité dans le cache.

L'attribut optionnel region permet de préciser le nom de la région à utiliser dans le cache. Par défaut, c'est le nom pleinement qualifié de la classe de l'entité.

L'attribut optionnel include peut prendre deux valeurs : all et non-lazy. La valeur non-lazy permet de ne pas mettre en cache des entités chargées de manière lazy. La valeur par défaut est all.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate
Mapping DTD//EN"
 "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.jmdoudoux.test.hibernatecache.entity.Pays" table="pays" >
        <cache usage="read-only"/>
        <id name="id" column="id" type="int" >
            <generator class="identity" />
        </id>
        <property name="codeIso" column="code_iso" not-null="true" type="string" />
        <property name="nom" not-null="true" type="string" />
        <many-to-one name="devise" column="FK_DEVISE"
           class="com.jmdoudoux.test.hibernatecache.entity.Devise" />
    </class>
</hibernate-mapping>

L'utilisation du cache de second niveau doit être activée et configurée comme indiqué dans la section concernée.

Résultat :
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=4 codeIso=LU nom=Luxembourg

La requête n'est plus effectuée qu'une seule fois sur la base de données : pour les autres accès, les données de l'entité sont extraites du cache.

Dans la classe de l'entité, il est possible d'utiliser le tag @org.hibernate.annotations.Cache.

Exemple :
package com.jmdoudoux.test.hibernatecache.entity;

import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "PAYS")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Pays implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  private int               id;

  @Column(name = "code_iso")
  private String            codeIso;

  @Basic
  private String            nom;

  @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
  @JoinColumn(name = "FK_DEVISE")
  private Devise            devise;

  public Pays() {
  }

  // getters et setters
  
  @Override
  public String toString() {
    return "Pays [id=" + id + ", codeIso=" + codeIso + ", nom=" + nom
        + ", devise=" + devise + ", getClass()=" + getClass() + ", hashCode()="
        + hashCode() + ", toString()=" + super.toString() + "]";
  }
}

L'attribut obligatoire usage permet de préciser la stratégie de gestion des accès concurrents grâce à l'énumération org.hibernate.annotations.CacheConcurrencyStrategy qui possède les valeurs NONE, READ_ONLY, READ_WRITE, NONSTRICT_READ_ONLY et TRANSACTIONAL.

L'attribut optionnel region permet de préciser le nom de la région du cache à utiliser. Par défaut, c'est le nom pleinement qualifié de la classe de l'entité.

L'attribut optionnel include peut prendre deux valeurs : all pour inclure toutes les propriétés ou non-lazy pour inclure uniquement les propriétés chargées de manière différée.

Enfin, avec le fichier de configuration hibernate.cfg.xml, il est possible d'utiliser le tag <class-cache> pour les entités.

Le tag <class-cache> possède deux attributs :

  • class : permet de préciser le nom pleinement qualifié de la classe de l'entité
  • usage : permet de préciser la stratégie de gestion des accès concurrents
Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mabdd</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password"></property>
    <property name="hibernate.connection.pool_size">1</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Cache de second niveau désactivé  -->
    <!-- property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property-->
    <property name="hibernate.cache.region.factory_class">
      org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
    <property name="hibernate.generate_statistics">true</property>
    <property name="hibernate.cache.use_structured_entries">true</property>
    <!-- Afficher les requêtes SQL exécutées sur la sortie standard -->
    <property name="hibernate.show_sql">true</property>
    <mapping resource="Pays.hbm.xml" />
    <mapping resource="Devise.hbm.xml" />
    <class-cache class="com.jmdoudoux.test.hibernatecache.entity.Pays" usage="read-only"/>
  </session-factory>
</hibernate-configuration>

Les méthodes load() et get() vérifient toujours le cache avant d'exécuter une requête sur la base de données.

Le cache de second niveau ne stocke pas une instance d'une entité mais une forme dite déshydratée (dehydrated) d'une entité : ce sont les valeurs de chaque propriété qui sont stockées dans le cache. Si cette propriété est du type d'une entité (dans une association de type xxx-to-one), alors c'est l'identifiant de cette entité qui est stocké dans le cache.

Seules les données de l'entité elle-même sont mises en cache : les entités de ses associations ne sont pas mises dans le cache par défaut sauf si les entités correspondantes sont explicitement configurées pour y être placées.

A chaque fois qu'une instance de l'entité doit être obtenue du cache, une nouvelle instance est créée à partir des valeurs des propriétés stockées dans le cache.

Il est donc très important de tenir compte du fait que si de nombreuses entités sont obtenues du cache de second niveau alors il y a un surcoût lié à chaque fois à la création d'une nouvelle instance.

Le cache de second niveau ne contient pas de graphe d'objets : les relations ne contiennent que les identifiants des entités. Ceci permet à Hibernate de ne pas répliquer de données.

Une exception de type java.lang.UnsupportedOperationException avec le message «Can't write to a readonly object»  est levée si une modification est faite sur une entité dont la stratégie de cache est read-only.

Si une entité stockée en cache doit être mise à jour, il faut configurer sa mise en cache en utilisant une stratégie le permettant. Par exemple, la stratégie read-write permet de mettre à jour les données de l'entité mise en cache.

Exemple :
  public static void lireEtModifierPays() throws Exception {
    Session session1 = sessionFactory.openSession();
    Session session2 = null;
    Transaction tx = null;
    try {
      tx = session1.beginTransaction();
      Pays pays = (Pays) session1.load(Pays.class, new Integer(99));
      System.out.println("session1 pays : id=" + pays.getId() + " codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      pays.setNom(pays.getNom() + " modifie");
      tx.commit();
      session2 = sessionFactory.openSession();
      tx = session2.beginTransaction();
      pays = (Pays) session2.load(Pays.class, new Integer(99));
      System.out.println("session2 pays : id=" + pays.getId() +" codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      if (session2 != null)
        session2.close();
      if (session1 != null)
        session1.close();
    }
  }

Résultat :
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
session1 pays : id=99 codeIso=XXX nom=test
Hibernate: update pays set
code_iso=?, nom=?, FK_DEVISE=? where id=?
session2 pays : id=99 codeIso=XXX nom=test modifie
2012-08-28 22:40:01.982 INFO    
[main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000239:
Second level cache puts: 2
2012-08-28 22:40:01.998 INFO    
[main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000237:
Second level cache hits: 1
2012-08-28 22:40:01.998
INFO    
[main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000238:
Second level cache misses: 1

Dans l'exemple ci-dessus, l'entité est mise deux fois dans le cache de second niveau : une première fois après la lecture et une seconde fois après la mise à jour de l'entité. Lorsque l'entité est relue dans la seconde session, l'entité est obtenue du cache de second niveau des entités.

Cet exemple fonctionne comme attendu car les deux sessions ne sont pas imbriquées.

Exemple :
  public static void lireEtModifierPays() throws Exception {
    Session session1 = sessionFactory.openSession();
    Session session2 = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session1.beginTransaction();
      Pays pays = (Pays) session1.load(Pays.class, new Integer(99));
      System.out.println("session1 pays : id=" + pays.getId() + " codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      pays.setNom(pays.getNom() + " modifie");
      tx.commit();
      tx = session2.beginTransaction();
      pays = (Pays) session2.load(Pays.class, new Integer(99));
      System.out.println("session2 pays : id=" + pays.getId() + " codeIso="
          + pays.getCodeIso() + " nom=" + pays.getNom());
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      if (session2 != null)
        session2.close();
      if (session1 != null)
        session1.close();
    }
  }

Résultat :
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
2012-08-19 21:55:51.721 INFO     
session1 pays : id=99 codeIso=XXX nom=test modifie
Hibernate: update pays set
code_iso=?, nom=?, FK_DEVISE=? where id=?
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
session2 pays : id=99 codeIso=XXX nom=test modifie modifie

Dans l'exemple ci-dessus, l'entité est mise deux fois dans le cache de second niveau : une première fois après la lecture et une seconde fois après la mise à jour de l'entité. Lorsque l'entité est relue dans la seconde session, l'entité n'est pas obtenue du cache de second niveau des entités mais elle est relue de la base de données.

Hibernate vérifie que la date de création de la session est supérieure à la date de mise en cache de l'entité : si ce n'est pas le cas, la donnée est relue de la base de données.

Dans l'exemple ci-dessus, l'entité est mise deux fois dans le cache de second niveau mais elle n'est jamais obtenue du cache, ce qui est contraire à ce qui pourrait être voulu.

La stratégie nonstrict-read-write permet aussi de mettre à jour un entité mise en cache mais les données du cache ne sont pas modifiées : les données de l'entité son simplement invalidées dans le cache, ce qui reforce une lecture pour le prochain accès à l'entité.

Résultat :
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
session1 pays : id=99 codeIso=XXXtest nom=test modifie
Hibernate: update pays set
code_iso=?, nom=?, FK_DEVISE=? where id=?
Hibernate: select pays0_.id as
id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE
as FK4_0_0_ from pays pays0_ where pays0_.id=?
session2 pays : id=99 codeIso=XXXtest nom=test modifie
2012-08-28 22:43:53.960 INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl
- HHH000239: Second level cache puts: 2
2012-08-28 22:43:53.960 INFO    
[main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000237:
Second level cache hits: 0
2012-08-28 22:43:53.960
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl
- HHH000238: Second level cache misses: 2

 

56.12.6. Le cache des associations many

Le cache des associations est utilisé lors du parcours des relations de type xxxToMany.

Par défaut, les associations ne sont pas mises dans le cache de second niveau par Hibernate. Hibernate laisse la possibilité de choisir les associations qui doivent être mises en cache ou être systématiquement relues de la base de données. Celles qui doivent donc être mises en cache doivent être configurées comme telles.

L'exemple de cette section va afficher la liste des pays de la base de données qui utilisent l'euro, trois fois de suite. Cette opération va exploiter la relation 1-N entre devise et pays.

Exemple :
package com.jmdoudoux.test.hibernatecache;

import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import com.jmdoudoux.test.hibernatecache.entity.Devise;
import com.jmdoudoux.test.hibernatecache.entity.Pays;

public class TestHibernateCache {
  private static SessionFactory sessionFactory = null;
  public static void main(String[] args) {
    try {
      sessionFactory = new Configuration().configure().buildSessionFactory();
      for (int i = 0; i < 3; i++) {
        // lirePays();
        listerPaysEuro();
      }
    } catch (Throwable ex) {
      System.err.println("Erreur durant les traitements" + ex);
    } finally {
      sessionFactory.close();
    }
  }

  public static void listerPaysEuro() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      Devise deviseEur = (Devise) session.load(Devise.class, new Integer(1));
      System.out.println("Devise : id=" + deviseEur.getId() + " code="
          + deviseEur.getCode() + " libelle=" + deviseEur.getLibelle());
      Set<Pays> paysEur = deviseEur.getPays();
      for (Pays pays : paysEur) {
        System.out.println("  pays : id=" + pays.getId() + " codeIso="
            + pays.getCodeIso() + " nom=" + pays.getNom());
      }
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }
}

Résultat :
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=3 codeIso=I nom=Italie
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=1 codeIso=FR nom=France
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=3 codeIso=I nom=Italie
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=1 codeIso=FR nom=France
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=3 codeIso=I nom=Italie

A chaque itération, la devise est lue dans la base de données et une requête est effectuée sur la base de données pour obtenir les pays possédant cette devise.

Pour limiter le nombre de requêtes effectuées sur la base de données, il est possible d'utiliser le cache des associations.

Lors du parcours des éléments de l'association, Hibernate stocke dans le cache les identifiants des entités mais pas les entités elles-mêmes. Si une entité associée est configurée pour être mise en cache, alors les données de l'entité sont retrouvées dans le cache, sinon elle sera relue de la base de données.

Le cache des associations ne stockent que des identifiants : pour l'entité et pour ses entités associées. De ce fait, si le cache d'associations est activé alors il est important d'activité aussi le cache sur les entités correspondantes pour éviter de gérer des requêtes inutiles sur la base de données.

Par défaut, aucune association n'est mise dans le cache de second niveau qui a été activé et configuré. Chaque association concernée doit être configurée pour être stockée dans le cache de second niveau. Cette configuration doit permettre de préciser la stratégie de gestion des accès concurrents aux données des associations.

Il existe plusieurs manières de déclarer qu'une association doit être mise dans le cache de second niveau :

  • utiliser le tag <cache> dans le fichier de mapping hbm.xml
  • utiliser l'annotation @Cache sur la collection dans la classe de l'entité
  • utiliser le tag <collection-cache> dans le fichier de configuration d'Hibernate

Une seule de ces solutions doit être utilisée.

Dans le fichier de mapping, la définition de la mise en cache de l'association se fait en utilisant le tag <cache>

Exemple :
<class name="Devise" table="DEVISE">
  <cache usage="read-only"/>
  <id name="id" column="ID">
    <generator class="identity"/>
  </id>
  <property name="code" column="CODE"/>
  <property name="libelle" column="LIBELLE"/>
  <set name="pays">
    <cache usage="read-only"/>
    <key column="FK_DEVISE"/>
    <one-to-many class="Pays"/>
  </set>
</class>

Attention : il est nécessaire que le cache pour les entités des deux relations soit actif puisque le cache des associations ne stocke que les identifiants des entités. Ceci évitera à Hibernate de refaire une lecture pour chacune des entités car il pourra directement obtenir les entités du cache.

Exemple :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate
Mapping DTD//EN" "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.jmdoudoux.test.hibernatecache.entity.Pays"
    table="pays" >
    <cache usage="read-only"/>
    <id name="id" column="id" type="int" >
      <generator class="identity" />
    </id>
    <property name="codeIso" column="code_iso"
      not-null="true" type="string" />
    <property name="nom" not-null="true" type="string" />
    <many-to-one name="devise" column="FK_DEVISE"
      class="com.jmdoudoux.test.hibernatecache.entity.Devise" />
  </class>
</hibernate-mapping>

Dans le fichier de configuration hibernate.cfg.xml, il est possible d'utiliser le tag <collection-cache> pour les associations.

Le tag <collection-cache> possède deux attributs :

  • collection : permet de préciser le nom de la collection
  • usage : permet de préciser la stratégie de gestion des accès concurrents
Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mabdd</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password"></property>
    <property name="hibernate.connection.pool_size">1</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Cache de second niveau désactivé  -->
    <!-- property name="hibernate.cache.provider_class">
      org.hibernate.cache.internal.NoCacheProvider</property-->
    <property name="hibernate.cache.region.factory_class">
      org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
    <property name="hibernate.generate_statistics">true</property>
    <property name="hibernate.cache.use_structured_entries">true</property>
    <!-- Afficher les requêtes SQL exécutées sur la sortie standard -->
    <property name="hibernate.show_sql">true</property>
    <mapping resource="Pays.hbm.xml" />
    <mapping resource="Devise.hbm.xml" />
    <class-cache class="com.jmdoudoux.test.hibernatecache.entity.Pays" usage="read-only"/>
    <class-cache class="com.jmdoudoux.test.hibernatecache.entity.Devise" usage="read-only" />
    <collection-cache collection="com.jmdoudoux.test.hibernatecache.entity.Devise.pays" 
      usage="read-only"/>
  </session-factory>
</hibernate-configuration>

Pour mettre en cache les données d'une relation OneToMany ou ManyToMany, il est possible d'utiliser l'annotation @Cache.

Exemple :
package com.jmdoudoux.test.hibernatecache.entity;

import java.io.Serializable;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "DEVISE")
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Devise implements Serializable {
  private static final long serialVersionUID = 1L;
  
  @Id
  private int               id;

  @Basic
  private String            code;

  @Basic
  private String            libelle;

  @OneToMany(mappedBy = "devise")
  @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
  private Set<Pays>         pays;

  public Devise() {
  }

  // getters et setters
}

L'utilisation du cache de second niveau doit être activée et configurée comme indiqué dans la section concernée.

Résultat :
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as CODE1_0_,
devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=3 codeIso=I nom=Italie
  pays : id=1 codeIso=FR nom=France
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=4 codeIso=LU nom=Luxembourg
Devise : id=1 code=EUR libelle=Euro
  pays : id=3 codeIso=I nom=Italie
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
Devise : id=1 code=EUR libelle=Euro
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=3 codeIso=I nom=Italie

Les deux requêtes ne sont plus effectuées qu'une seule fois sur la base de données : pour les autres accès, les données de l'entité sont extraites du cache des associations et du cache des entités.

Si le cache n'est pas activé sur l'entité possédant la relation many-to-one, alors l'entité est relue systématiquement dans la base de données malgré l'activation du cache sur la relation.

Résultat :
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=3 codeIso=I nom=Italie
  pays : id=1 codeIso=FR nom=France
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
  pays : id=3 codeIso=I nom=Italie
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=1 codeIso=FR nom=France
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
  pays : id=3 codeIso=I nom=Italie
  pays : id=2 codeIso=D nom=Allemagne

Dans ce cas, l'utilisation du cache perd toute son efficacité. La requête pour obtenir les identifiants de la relation n'est bien effectuée qu'une seule fois et les données des invocations suivantes sont obtenues du cache. Cependant, comme l'entité pays n'est pas en cache, Hibernate qui ne possèdent que l'identifiant est obligé de refaire une lecture dans la base de données pour chaque entité.

C'est le même comportement si le cache sur l'entité pays est activé mais le cache sur l'entité devise ne l'est pas.

Résultat :
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=3 codeIso=I nom=Italie
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
  pays : id=3 codeIso=I nom=Italie
  pays : id=1 codeIso=FR nom=France
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=2 codeIso=D nom=Allemagne
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
  pays : id=3 codeIso=I nom=Italie
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France

A chaque itération la devise est relue. La requête de la relation n'est effectuée qu'une seule fois puisque les identifiants de son résultat sont mis en cache ainsi que les entités pays correspondantes.

La pire en termes de nombre de requêtes effectuées sur la base survient si le cache est activé sur la relation mais ne l'est pas pour les deux entités.

Résultat :
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.FK_DEVISE as FK4_1_1_, pays0_.id as id1_,
pays0_.id as id0_0_, pays0_.code_iso as code2_0_0_, pays0_.nom as nom0_0_,
pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_ where pays0_.FK_DEVISE=?
  pays : id=2 codeIso=D nom=Allemagne
  pays : id=3 codeIso=I nom=Italie
  pays : id=1 codeIso=FR nom=France
  pays : id=4 codeIso=LU nom=Luxembourg
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays pays0_
where pays0_.id=?
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=1 codeIso=FR nom=France
  pays : id=3 codeIso=I nom=Italie
  pays : id=2 codeIso=D nom=Allemagne
Hibernate: select devise0_.ID as ID1_0_, devise0_.CODE as
CODE1_0_, devise0_.LIBELLE as LIBELLE1_0_ from DEVISE devise0_ where
devise0_.ID=?
Devise : id=1 code=EUR libelle=Euro
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
  pays : id=3 codeIso=I nom=Italie
  pays : id=1 codeIso=FR nom=France
  pays : id=4 codeIso=LU nom=Luxembourg
  pays : id=2 codeIso=D nom=Allemagne

Dans ce cas, la requête de la relation n'est effectuée qu'une seule fois et les identifiants sont mis dans le cache. Pour les itérations suivantes, Hibernate est obligé de relire les entités devises et pays puisque les identifiants correspondants ne sont pas trouvés dans le cache des entités.

 

56.12.7. Le cache des requêtes

La mise en cache des résultats d'une requête n'apporte pas toujours un gain significatif. Ce sont essentiellement les requêtes qui sont fréquemment exécutées avec les mêmes paramètres qu'il est intéressant de mettre en cache.

Par défaut, l'activation du cache de second niveau ne met aucune requête dans le cache. Comme la plupart des requêtes ne tireront pas de bénéfice à être mises en cache, aucune ne l'est par défaut. Il faut explicitement demander la mise en cache d'une requête.

Hibernate utilise une combinaison de la requête SQL exécutée et des valeurs des paramètres de cette requête pour composer la valeur de la clé dans le cache. La valeur associée à cette clé est la liste des identifiants des entités retournées par la requête. Ainsi, si la requête est de nouveau invoquée avec les mêmes paramètres, alors les identifiants des entités en résultat seront directement retrouvés dans le cache.

Pour renvoyer le résultat du cache des requêtes, Hibernate va rechercher les entités dans le cache des entités à partir des identifiants stockés dans le cache des requêtes. Si l'entité est trouvée, alors une nouvelle instance est créée à partir des informations du cache (cache de premier niveau ou cache des entités) sinon l'entité est relue dans la base de données.

L'exemple de cette section va afficher la liste de tous les pays de la base de données, trois fois de suite. Cette opération va utiliser l'API Criteria qui génère une requête HQL, elle-même transformée en requête SQL exécutée sur la base de données.

Exemple :
package com.jmdoudoux.test.hibernatecache;

import java.util.List;
import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import com.jmdoudoux.test.hibernatecache.entity.Devise;
import com.jmdoudoux.test.hibernatecache.entity.Pays;

public class TestHibernateCache {
  private static SessionFactory sessionFactory = null;

  public static void main(String[] args) {
    try {
      sessionFactory = new Configuration().configure().buildSessionFactory();
      for (int i = 0; i < 3; i++) {
        listerTousPays();
      }
    } catch (Throwable ex) {
      System.err.println("Erreur durant les traitements" + ex);
    } finally {
      sessionFactory.close();
    }
  }

  public static void listerTousPays() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      List<Pays> listPays = session.createCriteria(Pays.class).list();
      for (Pays pays : listPays) {
        System.out.println("pays : id=" + pays.getId() + " codeIso="
            + pays.getCodeIso() + " nom=" + pays.getNom());
      }
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }
}

Résultat :
Hibernate: select this_.id as id0_0_, this_.code_iso as
code2_0_0_, this_.nom as nom0_0_, this_.FK_DEVISE as FK4_0_0_ from pays this_
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
Hibernate: select this_.id as id0_0_, this_.code_iso as
code2_0_0_, this_.nom as nom0_0_, this_.FK_DEVISE as FK4_0_0_ from pays this_
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
Hibernate: select this_.id as id0_0_, this_.code_iso as code2_0_0_,
this_.nom as nom0_0_, this_.FK_DEVISE as FK4_0_0_ from pays this_
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon\n

A chaque itération, la requête est effectuée sur la base de données pour obtenir les pays. Pour limiter le nombre de requêtes effectuées, il est possible d'utiliser le cache des requêtes.

Pour utiliser le cache des requêtes, il faut :

  • activer et configurer le cache de second niveau (l'implémentation du cache utilisé doit proposer un support du cache des requêtes d'Hibernate)
  • activer l'utilisation du cache des requêtes en passant la valeur true à la propriété hibernate.cache.use_query_cache dans le fichier de configuration d'Hibernate
  • activer le cache sur les entités qui seront renvoyées par les requêtes mises en cache
Exemple :
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration SYSTEM 
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- ...  -->
    <!-- Cache de second niveau activé avec ehcache  -->
    <property name="hibernate.cache.region.factory_class">
      org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
    <!-- Activation du cache des requêtes  -->
    <property name="hibernate.cache.use_query_cache">true</property>
    <!-- ...  -->
  </session-factory>
</hibernate-configuration>

Pour chaque requête que l'on souhaite mettre en cache, il faut invoquer la méthode setCacheable() en lui passant la valeur true en paramètre. La mise en cache des requêtes peut être utilisée sur des objets de type Criteria et Query.

Exemple :
  public static void listerTousPays() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      List<Pays> listPays = session.createCriteria(Pays.class)
        .setCacheable(true).list();
      for (Pays pays : listPays) {
        System.out.println("pays : id=" + pays.getId() + " codeIso="
            + pays.getCodeIso() + " nom=" + pays.getNom());
      }
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }

Résultat :
Hibernate: select pays0_.id as id0_, pays0_.code_iso as code2_0_,
pays0_.nom as nom0_, pays0_.FK_DEVISE as FK4_0_ from pays pays0_
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon\n

La requête n'est exécutée qu'une seule fois car pour les itérations suivantes les données sont extraites du cache des requêtes et du cache des entités.

Le mode de fonctionnement est identique pour des requêtes HQL.

Exemple :
  public static void listerTousPays() throws Exception {
    Session session = sessionFactory.openSession();
    Transaction tx = null;
    try {
      tx = session.beginTransaction();
      Query query = session.createQuery("from Pays").setCacheable(true);
      List<Pays> listPays = query.list();
      for (Pays pays : listPays) {
        System.out.println("pays : id=" + pays.getId() + " codeIso="
            + pays.getCodeIso() + " nom=" + pays.getNom());
      }
      tx.commit();
    } catch (Exception e) {
      if (tx != null) {
        tx.rollback();
      }
      throw e;
    } finally {
      session.close();
    }
  }

Le résultat à l'exécution est le même mais le temps est réduit.

Le cache des requêtes ne stocke pas les entités qui sont retournées par la requêtes mais uniquement la valeur de leur identifiant et leur type. Pour tirer pleinement parti du cache des requêtes, il est donc important de le coupler avec l'utilisation du cache de second niveau des entités.

Il est important que le cache soit activé sur les entités en résultat des requêtes qui sont mises dans le cache des requêtes. La clé du cache est la requête et la valeur ne contient que les identifiants des entités. Celles-ci doivent être lues soit dans le cache des entités soit être relues de la base de données.

Dans l'exemple ci-dessous, l'entité Pays n'est pas mise en cache.

Résultat :
Hibernate:
select pays0_.id as id0_, pays0_.code_iso as code2_0_, pays0_.nom as nom0_,
pays0_.FK_DEVISE as FK4_0_ from pays pays0_
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
Hibernate: select pays0_.id as id0_0_, pays0_.code_iso as
code2_0_0_, pays0_.nom as nom0_0_, pays0_.FK_DEVISE as FK4_0_0_ from pays
pays0_ where pays0_.id=?
pays : id=1 codeIso=FR nom=France
pays : id=2 codeIso=D nom=Allemagne
pays : id=3 codeIso=I nom=Italie
pays : id=4 codeIso=LU nom=Luxembourg
pays : id=5 codeIso=GB nom=Grande Bretagne
pays : id=6 codeIso=US nom=Etats Unis
pays : id=7 codeIso=JA nom=Japon\n

La requête pour obtenir tous les pays est bien exécutée une seule fois et mise en cache. Malheureusement comme l'entité pays n'est pas mise en cache, Hibernate n'a pas d'autre solution que de relire chaque entité dans la base de données, ce qui a un effet catastrophique sur les performances car au lieu d'une seule requête sur la base de données, ce sont de nombreuses requêtes qui sont exécutées.

Pour éviter à Hibernate de faire des relectures pour chacune des entités, il est préférable d'activer la mise en cache des entités concernées. Il est donc important d'utiliser en conjonction le cache des requêtes et le cache des entités : le type des entités retournées par les résultats d'une requête mise en cache doit être configuré pour être pris en compte par le cache des entités.

Le cache des requêtes d'Hibernate utilise deux régions particulières par défaut :

  • org.hibernate.cache.StandardQueryCache : stocke les identifiants des entités retournées par une requête pour des paramètres donnés
  • org.hibernate.cache.UpdateTimestampsCache : stocke pour chaque table la date/heure de dernière mise à jour d'un enregistrement. Chaque modification dans une table met à jour la date de dernière modification dans l'entrée correspondante de cette région du cache.

Il est très important que la région UpdateTimestampsCache soit configurée pour ne jamais être invalidée périodiquement.

Il est possible de configurer une région dédiée du cache pour stocker le résultat de certaines requêtes. Cette configuration particulière peut notamment permettre de définir la stratégie d'éviction du contenu du cache. Par défaut, les requêtes mises en cache le sont dans la région par défaut pour les requêtes. Pour préciser une autre région, il faut préciser son nom en invoquant la méthode setCacheRegion().

Exemple :
      Query query = session.createQuery("from Pays").setCacheable(true)
        .setCacheRegion("query.pays");
      List<Pays> listPays = query.list();

Si plusieurs régions peuvent être utilisées pour mettre en cache les requêtes, il ne peut y avoir qu'une seule région de type timestamps.

La région UpdateTimestampsCache permet à Hibernate de stocker la date/heure de dernière mise à jour pour toutes les tables utilisées.

Hibernate utilise les données de la région UpdateTimestampCache pour invalider les données contenues dans la région StandardQueryCache : Hibernate invalide le contenu du cache pour une requête si, pour une des tables figurant dans le résultat, la date/heure de mise en cache de la requête est antérieure à celle de la dernière modification stockée dans la région UpdateTimestampsCache.

En d'autres termes, lorsqu'une requête qui a été mise en cache est réexécutée, Hibernate vérifie la date/heure de dernière modification (insert, update ou delete) dans la ou les tables utilisées comme résultat. Si cette date/heure est plus récente que la date/heure de mise en cache des résultats de la requête alors celle-ci est invalidée dans le cache et la requête est réexécutée sur la base de données.

La date/heure stockée dans la région UpdateTimestampsCache concerne une mise à jour sur n'importe quelle occurrence de la table.

La méthode setCacheMode() des interfaces Query et Criteria permet de modifier le mode d'utilisation du cache pour la requête selon l'instance de type CacheMode fournie en paramètre.

Instance 

Rôle

CacheMode.NORMAL 

Des éléments peuvent être lus et écrits dans le cache

CacheMode.GET 

Les éléments peuvent être uniquement lus du cache

CacheMode.PUT

Les éléments ne sont pas lus du cache mais ils sont écrits dans le cache lorsqu'ils sont lus dans la base de données

CacheMode.IGNORE 

Les éléments ne sont ni lus ni écrits dans le cache

CacheMode.REFRESH 

Forcer le rafraichissement des éléments du cache


En passant la valeur CacheMode.REFRESH à la méthode setCacheMode(), Hibernate ne va pas rechercher le résultat de la requête dans le cache mais exécuter la requête et insérer ou remplacer le résultat dans le cache. Ceci permet de forcer le rafraîchissement des données du cache : c'est particulièrement utile si un autre processus met à jour la base de données pour des données référencées dans le cache.

Il est nécessaire d'être prudent lors de l'utilisation du cache des requêtes.

Le cache des requêtes doit être utilisé avec soin car son activation peut introduire une certaine latence et limiter les possibilités de montée en charge.

Il est par exemple contre-productif :

  • d'activer le cache des requêtes si celui-ci n'est pas utilisé.
  • de mettre en cache les résultats d'une requête dont la ou les tables des entités concernées sont modifiées fréquemment impliquant de fait une invalidation des résultats dans le cache

Une modification dans une table implique la mise à jour du cache timestamps pour la table et invalide automatiquement les résultats d'une requête du cache qui contiennent au moins une entité de cette table même si la modification ne concerne pas cette entité. Si une table est souvent modifiée, le taux de hit dans le cache est très bas puisque les données du cache sont fréquemment invalidées.

Le cache des requêtes peut être assez consommateur en ressources surtout si de nombreuses requêtes avec leurs paramètres sont mises en cache. Les requêtes SQL sont généralement composées de plusieurs centaines de caractères fréquemment utilisées avec des paramètres différents.

La mise à jour du cache des timestamps introduit une contention liée à la pose d'un verrou. Ce verrou est mis en oeuvre à chaque accès au cache ce qui peut créer un goulot d'étranglement lorsque la charge augmente ou lorsque la même table est mise à jour par plusieurs threads.

L'utilisation du cache des requêtes induit donc un certain surcoût dans les traitements transactionnels. Ceci est particulièrement vrai si les entités mises en cache sont mises à jour car dans ce cas Hibernate doit invalider certaines données du cache.

 

56.12.8. La gestion du cache de second niveau

Jusqu'à la version 3.3 d'Hibernate incluse, la classe SessionFactory proposait plusieurs méthodes pour retirer des données du cache de second niveau.

Méthode

Rôle

void evict(Class persistentClass)

Retirer du cache toutes les données d'un type d'entité

void evict(Class persistentClass, Serializable id)

Retirer du cache les données d'une entité dont l'identifiant est fourni en paramètre

void evictCollection(String roleName)

Supprimer toutes les données d'une association dont le nom est fourni en paramètre

void evictCollection(String roleName, Serializable id)

Supprimer les données d'une association dont le nom est fourni en paramètres etconcernant l'entité dont l'identifiant est fourni

void evictEntity(String entityName)

Supprimer toutes les données du type d'entité dont le nom est fourni en paramètre

void evictEntity(String entityName, Serializable id)

Supprimer les données d'une entité dont le type et l'identifiant sont fournis en paramètres

void evictQueries()

Supprimer tout le contenu de la région par défaut du cache des requêtes

void evictQueries(String cacheRegion)

Supprimer tout le contenu d'une région particulière du cache des requêtes dont le nom est passé en paramètre


La méthode getAllClassMetadata() renvoie une map dont la clé est le nom de l'entité et la valeur est un objet de type ClassMetadata qui encapsule les métadonnées de l'entité.

La méthode getAllCollectionMetadata() renvoie une map dont la clé est le nom de l'association et la valeur est un objet de type CollectionMetadata qui encapsule les métadonnées de l'association.

L'exemple ci-dessous purge entièrement le contenu du cache de second niveau (associations, entités et requêtes).

Exemple :
Map<String, CollectionMetadata> collectionMetadatas = 
  sessionFactory.getAllCollectionMetadata();
for (String nom : collectionMetadatas.keySet()) {
  sessionFactory.evictCollection(nom);
}

Map<String, ClassMetadata> classMetadatas = sessionFactory.getAllClassMetadata();
for (String nom : classMetadatas.keySet()) {
  sessionFactory.evictEntity(nom);
}
     
sessionFactory.evictQueries();

A partir de la version 3.5 d'Hibernate, toutes les méthodes de l'interface SessionFactory relatives à la gestion du contenu du cache sont deprecated (evit(), evictCollection(), evictEntity(), evictQueries()) : il faut utiliser un objet de type Cache.

Un objet de type Cache est obtenu en invoquant la méthode getCache() de la SessionFactory. L'interface Cache décrit les fonctionnalités permettant de déterminer la présence et de supprimer des données dans les régions du cache de second niveau.

Méthode

Rôle

boolean containsCollection(String role, Serializable ownerIdentifier)

Renvoyer un booléen qui permet de préciser si une association est présente dans le cache

boolean containsEntity(Class entityClass, Serializable identifier)

Renvoyer un booléen qui permet de préciser si une entité est présente dans le cache

boolean containsEntity(String entityName, Serializable identifier)

Renvoyer un booléen qui permet de préciser si une entité est présente dans le cache

boolean containsQuery(String regionName)

Renvoyer un booléen qui permet de préciser si une région utilisée pour le cache des requêtes contient des éléments

void evictCollection(String role, Serializable ownerIdentifier)

Supprimer des éléments du cache de second niveau relatifs à une association

void evictCollectionRegion(String role)

Supprimer les données des entités du cache de second niveau relatives à une association

void evictCollectionRegions()

Supprimer tous les éléments des régions du cache relatives aux associations

void evictDefaultQueryRegion()

Supprimer les éléments de la région par défaut du cache des requêtes

void evictEntity(Class entityClass, Serializable identifier)

Supprimer les données d'une entité du cache de second niveau

void evictEntity(String entityName, Serializable identifier)

Supprimer les données d'une entité du cache de second niveau

void evictEntityRegion(Class entityClass)

Supprimer toutes les données d'une entité

void evictEntityRegion(String entityName)

Supprimer toutes les données d'une entité

void evictEntityRegions()

Supprimer toutes les données des caches des entités

void evictQueryRegion(String regionName)

Supprimer toutes les données de la région du cache des requêtes

void evictQueryRegions()

Supprimer toutes les données de toutes les régions utilisées pour le cache des requêtes


Attention : aucune de ces méthodes ne prend en compte un aspect transactionnel : leur exécution est immédiate sans gestion des accès concurrents réalisés par les transactions en cours.

Exemple :
Cache cache = sessionFactory.getCache();
     
System.out.println(cache.containsEntity(Pays.class, 2));
     
Cache.evictEntity(Pays.class,2);

 

56.12.9. Le monitoring de l'utilisation du cache

Une fois un cache de second niveau mis en place, il est nécessaire de régulièrement surveiller son activité pour vérifier sa bonne utilisation et éventuellement modifier sa configuration pour obtenir les meilleures performances.

L'utilisation du cache de second niveau implique que l'accès aux données de ce cache peut réussir (hit) ou échouer (miss).

Il est possible de demander à Hibernate d'être verbeux sur l'utilisation du cache dans les logs en utilisant le niveau DEBUG pour le logger org.hibernate.cache.

Exemple :
  <logger name="org.hibernate.cache">
    <level value="DEBUG" />
  </logger>

L'inconvénient de cette activation est qu'elle est très verbeuse.

Hibernate propose un mécanisme de calcul de statistiques sur son activité incluant entre autres des informations sur l'utilisation du cache. Ce mécanisme doit être activé en donnant la valeur true à la propriété hibernate.generate_statistics.

L'inconvénient des statistiques est qu'elles consomment de la ressource mais aussi qu'elles induisent une légère dégradation des performances liée à la gestion des accès concurrents.

Hibernate propose des statistiques sur l'utilisation de l'objet de type SessionFactory. Une API permet de les consulter et elles sont facilement exposables sous la forme d'un MBean JMX.

 

56.12.9.1. L'activation et l'obtention de données statistiques

Les statistiques permettent d'obtenir des informations utiles sur l'utilisation du cache et des sessions :

  • Obtenir des informations globales : le nombre d'entités obtenues du cache (hit), le nombre d'entités non obtenues du cache (miss), ...
  • Obtenir des informations précises sur l'utilisation dans le cache d'une entité ou d'une association particulière (EntityStatistics et CollectionStatistics)
  • Obtenir des informations sur l'exécution des requêtes mises en cache (QueryStatistics)
  • Obtenir des informations sur l'utilisation d'une région particulière du cache (SecondLevelCacheStatistics)

Hibernate propose des statistiques d'utilisation fournies par la classe SessionFactory. Ces informations sont disponibles de deux manières :

  • La publication par JMX en activant le MBean StatisticsService
  • L'utilisation de la méthode getStatistics() de la classe SessionFactory

Par défaut, les statistiques sont désactivées. Il y a deux manières de les activer :

  • en utilisant la propriété hibernate.generate_statistics du fichier de configuration d'Hibernate et en lui passant la valeur true
Exemple :
<prop key="hibernate.generate_statistics">true</prop>\n

  • en invoquant la méthode SessionFactory.getStatistics().setStatisticsEnabled() et en lui passant la valeur true en paramètre
Exemple :
Statistics statistics = sessionFactory.getStatistics();
statistics.setStatisticsEnabled(true);

Plusieurs méthodes concernent les statistiques sur l'utilisation des sessions :

Méthode

Rôle

long getCloseStatementCount()

Obtenir le nombre d'objets de type PrepareStatement qui ont été fermés

long getCollectionFetchCount()

 

long getCollectionLoadCount()

Obtenir le nombre d'associations lues de la base de données

long getCollectionRecreateCount()

 

long getCollectionRemoveCount()

Obtenir le nombre d'associations supprimées

String[] getCollectionRoleNames()

Obtenir le nom de toutes les associations

long getCollectionUpdateCount()

Obtenir le nombre d'associations mises à jour

long getConnectCount()

Obtenir le nombre de connexions demandées par les sessions

long getEntityDeleteCount()

Obtenir le nombre d'entités supprimées dans la base de données

long getEntityFetchCount()

 

long getEntityInsertCount()

Obtenir le nombre d'entités insérées dans la base de données

long getEntityLoadCount()

Obtenir le nombre d'entités lues de la base de données

String[] getEntityNames()

Obtenir le nom de toutes les entités

long getEntityUpdateCount()

Obtenir le nombre d'entités mises à jour

long getFlushCount()

Obtenir le nombre de flushes implicites ou explicites fait par les sessions

long getOptimisticFailureCount()

Obtenir le nombre d'exceptions de type StaleObjectStateExceptions qui sont levées

long getPrepareStatementCount()

Obtenir le nombre de PrepareStatement

String[] getQueries()

Obtenir les requêtes SQL exécutées

long getQueryExecutionCount()

Obtenir le nombre de requêtes exécutées

long getQueryExecutionMaxTime()

Obtenir le temps de la requête dont l'exécution est la plus longue

String getQueryExecutionMaxTimeQueryString()

Obtenir la requête dont le temps d'exécution est le plus long

long getSessionCloseCount()

Obtenir le nombre de sessions fermées

long getSessionOpenCount()

Obtenir le nombre de sessions ouvertes

long getSuccessfulTransactionCount()

Obtenir le nombre de transactions qui ont réussies

long getTransactionCount()

Obtenir le nombre de transactions utilisées


Plusieurs méthodes permettent d'obtenir le nom des régions utilisées par le cache de second niveau : getEntityNames(), getCollectionRoleNames(), getQueries() et getSecondLevelCacheRegionNames().

Plusieurs méthodes concernent l'utilisation du cache de second niveau et des régions qu'il utilise.

Méthode

Rôle

CollectionStatistics getCollectionStatistics(String role)

Obtenir des statistiques pour une association

EntityStatistics getEntityStatistics(String entityName)

Obtenir des statistiques pour une entité

long getQueryCacheHitCount()

Obtenir le nombre de requêtes obtenues du cache

long getQueryCacheMissCount()

Obtenir le nombre de requêtes non obtenues du cache

long getQueryCachePutCount()

Obtenir le nombre de requêtes mises en cache

QueryStatistics getQueryStatistics(String queryString)

Obtenir des statistiques pour une requête

long getSecondLevelCacheHitCount()

Obtenir le nombre d'entités et d'associations obtenues du cache

long getSecondLevelCacheMissCount()

Obtenir le nombre d'entités et d'associations non obtenues du cache et donc relues de la base de données

long getSecondLevelCachePutCount()

Obtenir le nombre d'entités et associations mises dans le cache

String[] getSecondLevelCacheRegionNames()

Obtenir le nom de toutes les régions du cache

SecondLevelCacheStatistics getSecondLevelCacheStatistics(String regionName)

Obtenir les statistiques d'utilisation d'une région


Plusieurs méthodes concernent la gestion des statistiques

Méthode

Rôle

void clear()

Réinitialiser toutes les valeurs de statistiques

long getStartTime()

Date/heure de démarrage (en ms) de calcul des statistiques

boolean isStatisticsEnabled()

Renvoyer un booléen qui précise si les statistiques sont calculées ou non

void logSummary()

Inscrire dans le journal un résumé des statistiques

void setStatisticsEnabled(boolean b)

Activer ou non le calcul des statistiques


La méthode clear() de la classe Statistics permet de réinitialiser les valeurs des statistiques calculées par Hibernate.

La méthode logSummary() de la classe Statistics permet d'envoyer dans le journal un résumé des statistiques calculées par Hibernate avec un niveau Info.

Résultat :
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000161:
Logging statistics....
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000251: 
Start time: 1342448010929
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000242:
Sessions opened: 3
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000241:
Sessions closed: 3
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000266:
Transactions: 3
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000258:
Successful transactions: 3
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000187: 
Optimistic lock failures: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000105:
Flushes: 3
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000048:
Connections obtained: 3
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000253:
Statements prepared: 1
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000252:
Statements closed: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000239:
Second level cache puts: 7
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000237:
Second level cache hits: 14
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000238:
Second level cache misses: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000079:
Entities loaded: 7
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000080:
Entities updated: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000078:
Entities inserted: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000076:
Entities deleted: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000077:
Entities fetched (minimize this): 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000033:
Collections loaded: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000036:
Collections updated: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000035:
Collections removed: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000034:
Collections recreated: 0
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000032: 
Collections fetched (minimize this): 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000438:
NaturalId cache puts: 0
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000439:
NaturalId cache hits: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000440:
NaturalId cache misses: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000441:
Max NaturalId query time: 0ms
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000442:
NaturalId queries executed to database: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000210:
Queries executed to database: 1
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000215:
Query cache puts: 1
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000433:
update timestamps cache puts: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000434:
update timestamps cache hits: 0
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000435:
update timestamps cache misses: 2
INFO     [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000213: 
Query cache hits: 2
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000214:
Query cache misses: 1
INFO    [main]:org.hibernate.stat.internal.ConcurrentStatisticsImpl - HHH000173:
Max query time: 455ms

Plusieurs classes permettent d'obtenir des informations sur une région particulière du cache.

Attention : les informations temporelles fournies par les statistiques possèdent une précision qui dépend de la JVM et est généralement est de 3 millisecondes ou plus.

La classe EntityStatistics encapsule des informations statistiques sur une entité du cache. Elle possède plusieurs méthodes pour obtenir les valeurs :

Méthodes

Rôles

long getLoadCount()

Nombre d'entités lues

long getFetchCount()

 

long getInsertCount()

Nombre d'entités ajoutées

long getDeleteCount()

Nombre d'entités supprimées

long getUpdateCount()

Nombre d'entités mises à jour

long getOptimisticFailureCount()

Nombre de verrous optimistes qui ont échoués


Exemple :
      Statistics stats = sessionFactory.getStatistics();
      EntityStatistics entityStats = stats
         .getEntityStatistics("com.jmdoudoux.test.hibernatecache.entity.Pays");
      System.out.println(entityStats.getFetchCount());
      System.out.println(entityStats.getLoadCount());
      System.out.println(entityStats.getInsertCount());
      System.out.println(entityStats.getUpdateCount());
      System.out.println(entityStats.getDeleteCount());
      System.out.println(entityStats.getOptimisticFailureCount());

La classe CollectionStatistics encapsule des informations statistiques sur une collection du cache. Elle possède plusieurs méthodes pour obtenir les valeurs :

Méthodes

Rôles

long getLoadCount()

 

long getFetchCount()

 

long getRecreateCount()

 

long getRemoveCount()

 

long getUpdateCount()

 

Exemple :
      Statistics stats = sessionFactory.getStatistics();
      CollectionStatistics collectionStats = stats
         .getCollectionStatistics("com.jmdoudoux.test.hibernatecache.entity.Devise.pays");
      System.out.println(collectionStats.getFetchCount());
      System.out.println(collectionStats.getLoadCount());
      System.out.println(collectionStats.getRecreateCount());
      System.out.println(collectionStats.getRemoveCount());
      System.out.println(collectionStats.getUpdateCount());

La classe QueryStatistics encapsule des informations statistiques sur une requête du cache. Elle possède plusieurs méthodes pour obtenir les valeurs :

Méthodes

Rôles

long getCacheHitCount()

Nombre d'objets retrouvés dans le cache lors des exécutions de cette requête

long getCacheMissCount()

Nombre d'objets non retrouvés dans le cache lors des exécutions de cette requête

long getCachePutCount()

Nombre d'objets mis dans le cache suite aux exécutions de cette requête

long getExecutionAvgTime()

Temps moyen d'exécution de la requête

long getExecutionCount()

Nombre d'invocations de la requête

long getExecutionMaxTime()

Temps maximum d'exécution de la requête

long getExecutionMinTime()

Temps minimum d'exécution de la requête

long getExecutionRowCount()

Nombre d'entités retournées par toutes les invocations de la requête


Exemple :
      Statistics stats = sessionFactory.getStatistics();
      QueryStatistics queryStats = stats.getQueryStatistics("from Pays");
      System.out.println(queryStats.getCacheHitCount());
      System.out.println(queryStats.getCacheMissCount());
      System.out.println(queryStats.getCachePutCount());
      System.out.println(queryStats.getExecutionAvgTime());
      System.out.println(queryStats.getExecutionCount());
      System.out.println(queryStats.getExecutionMaxTime());
      System.out.println(queryStats.getExecutionMinTime());
      System.out.println(queryStats.getExecutionRowCount());

La classe SecondLevelCacheStatistics encapsule des informations statistiques sur l'utilisation d'une région du cache. Elle possède plusieurs méthodes pour obtenir les valeurs :

Méthodes

Rôles

long getHitCount()

Nombre d'éléments obtenus de la région

long getMissCount()

Nombre d'éléments non obtenus de la région

long getPutCount()

Nombre d'éléments insérés dans la région

long getElementCountInMemory()

Nombre d'éléments dans la région en mémoire

long getElementCountOnDisk()

Nombre d'éléments dans la région stockés sur disque

long getSizeInMemory()

Nombre d'octets consommés en mémoire par la région

Map getEntries()

Obtenir les éléments contenus dans la région du cache


Il est préférable de mettre la valeur true à la propriété hibernate.cache.use_structured_entries si la méthode getEntries() est utilisée.

Exemple :
      Statistics stats = sessionFactory.getStatistics();
      SecondLevelCacheStatistics cacheStats = stats
        .getSecondLevelCacheStatistics("com.jmdoudoux.test.hibernatecache.entity.Pays");
      System.out.println(cacheStats.getElementCountInMemory());
      System.out.println(cacheStats.getElementCountOnDisk());
      System.out.println(cacheStats.getEntries());
      System.out.println(cacheStats.getHitCount());
      System.out.println(cacheStats.getMissCount());
      System.out.println(cacheStats.getPutCount());
      System.out.println(cacheStats.getSizeInMemory());

Il est possible d'exposer les statistiques via JMX en utilisant un Mbean de type StatisticsServiceMBean. Il suffit d'enregistrer dans le serveur de Mbeans une instance de type StatisticsService. Sa méthode setSessionFactory() permet de fournir en paramètre la SessionFactory dont les statistiques doivent être exposées.

Exemple :
      MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
      ObjectName objName = new ObjectName("Hibernate:application=Statistics");
      StatisticsService statsMBean = new StatisticsService();
      statsMBean.setSessionFactory(sessionFactory);
      statsMBean.setStatisticsEnabled(true);
      mBeanServer.registerMBean(statsMBean, objName);

 

56.13. Les outils de génération de code

Hibernate fournit séparément un certain nombre d'outils. Ces outils sont livrés dans un fichier nommé hibernate-extensions-2.1.zip.

Il faut télécharger et décompresser le contenu de cette archive par exemple dans le répertoire où Hibernate a été décompressé.

L'archive contient deux répertoires :

  • hibern8ide
  • tools

Le répertoire tools propose trois outils :

  • class2hbm :
  • ddl2hbm :
  • hbm2java :

Pour utiliser ces outils, il y a deux solutions possibles :

  • utiliser les fichiers de commandes .bat fournis dans le répertoire /tools/bin
  • utiliser ant pour lancer ces outils

Pour utiliser les fichiers de commandes .bat, il est nécessaire au préalable de configurer les paramètres dans le fichier setenv.bat. Il faut notamment correctement renseigner les valeurs associées aux variables JDBC_DRIVER qui précise le pilote de la base de données et HIBERNATE_HOME qui précise le répertoire où est installé Hibernate.

Pour utiliser les outils dans un script ant, il faut créer ou modifier un fichier build en ajoutant une tâche pour l'outil à utiliser.

Il faut copier le fichier hibernate2.jar et les fichiers contenus dans le répertoire /lib d'Hibernate dans le répertoire lib du projet. Il faut aussi copier dans ce répertoire les fichiers contenus dans le répertoire /tools/lib et le fichier /tools/hibernate-tools.jar.

Pour éviter les messages d'avertissement sur la configuration manquante de log4j, le plus simple est de copier le fichier /src/log4j.properties d'Hibernate dans le répertoire bin du projet.

Hibernate permet aussi de créer la structure du schéma d'une base de base de données en utilisant la configuration du mapping des entités grâce à un outil nommé hbm2ddl.

Il faut utiliser la propriété hibernate.hbm2ddl.auto qui peut prendre plusieurs valeurs :

  • validate: valider la structure du schéma sans faire de modification dans la base de données
  • update: mettre à jour le schéma
  • create: créer le schéma en supprimant celui existant
  • create-drop: créer le schéma et le supprimer lorsque la sessionFactory est fermée
Exemple :
<hibernate-configuration>

  <session-factory> 
...
    <property name="hbm2ddl.auto">create</property> 
...                            
  </session-factory> 
</hibernate-configuration>

Lorsque les valeurs create et create-drop sont utilisées, il est possible de demander à Hibernate d'initialiser des données dans le schéma créé. Par défaut, Hibernate recherche un fichier import.sql dans le classpath. Si celui-ci est trouvé, Hibernate exécute les ordres SQL qu'il contient.

Depuis la version 3.6 d'Hibernate, la propriété hibernate.hbm2ddl.import_files permet de préciser un ou plusieurs fichiers qui seront utilisés pour créer les données. Si plusieurs fichiers sont indiqués, il faut les séparer avec un caractère virgule. Ils seront exécutés dans leur ordre de définition.

Exemple :
<hibernate-configuration>
 <session-factory>
...
  <property name="hbm2ddl.auto">create</property>
  <property name="hbm2ddl.import_files">/import-donnees-1.sql, /import-donnees-2.sql</property>
...
 </session-factory>
</hibernate-configuration>

Résultat :
INFO : org.hibernate.tool.hbm2ddl.SchemaExport -
Running hbm2ddl schema export
INFO : org.hibernate.tool.hbm2ddl.SchemaExport -
exporting generated schema to database
INFO : org.hibernate.tool.hbm2ddl.SchemaExport -
Executing import script: /import-donnees-1.sql
INFO : org.hibernate.tool.hbm2ddl.SchemaExport -
Executing import script: /import-donnees-2.sql
INFO : org.hibernate.tool.hbm2ddl.SchemaExport -
schema export complete

Cette fonctionnalité est particulièrement utile pour des tests automatisés mais n'est pas recommandée en production quand la gestion des évolutions du schéma doit être effectuée de manière plus attentive.

 

en construction
La suite de ce chapitre sera développée dans une version future de ce document

 


Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]