Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | 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 https://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 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 :
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;
}
} |
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 :
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.
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 :
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=org.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">org.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">org.hibernate.dialect.MySQLDialect</property>
<property name="show_sql">true</property>
<mapping resource="Personnes.hbm.xml"/>
</session-factory>
</hibernate-configuration> |
Pour utiliser Hibernate dans le code, il est nécessaire de réaliser plusieurs opérations :
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 :
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 org.hibernate.*;
import org.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 org.hibernate.*;
import org.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().
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 org.hibernate.*;
import org.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: org.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: org.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> |
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 :
Exemple : |
import org.hibernate.*;
import org.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 |
Hibernate utilise plusieurs moyens pour obtenir des données de la base de données :
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).
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 :
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) |
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 org.hibernate.*;
import org.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 org.hibernate.*;
import org.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 org.hibernate.*;
import org.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 org.hibernate.*;
import org.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 org.hibernate.*;
import org.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.
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 :
Elle propose des classes et des interfaces qui encapsulent les fonctionnalités de SQL dont les principales sont :
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".
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 :
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.
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(); |
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.
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.
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]);
}
|
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 |
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"));
|
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 fr.jmdoudoux.dej.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 :
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 fr.jmdoudoux.dej.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(); |
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.
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 :
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.
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.
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"); |
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 :
Les relations du monde relationnel possèdent quelques caractéristiques :
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 :
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.
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 :
Il y a plusieurs façons de traiter ce cas avec une ou deux tables dans la base de données et Hibernate :
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 ; |
Les classes qui encapsulent l'entité personne et les données de l'adresse sont de simples POJO.
Exemple : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 :
Exemple : |
package fr.jmdoudoux.dej.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) |
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 fr.jmdoudoux.dej.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 :
Exemple : |
package fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.Personne"></mapping>
<mapping class="fr.jmdoudoux.dej.hibernate.Adresse"></mapping>
</session-factory>
</hibernate-configuration>
|
L'application de test est basique :
Exemple : |
package fr.jmdoudoux.dej.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) |
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; |
Les classes qui encapsulent les entités personne et adresse sont de simples POJO.
Exemple : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 :
Exemple : |
package fr.jmdoudoux.dej.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) |
Le POJO qui encapsule une personne a quelques particularités relatives à la relation avec l'adresse :
Exemple : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 :
Exemple : |
package fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.Personne"></mapping>
<mapping class="fr.jmdoudoux.dej.hibernate.Adresse"></mapping>
</session-factory>
</hibernate-configuration>
|
L'application de test est basique :
Exemple : |
package fr.jmdoudoux.dej.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 [fr.jmdoudoux.dej.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(): fr.jmdoudoux.dej.hibernate.Adresse » lors de l'exécution.
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 ; |
Les classes qui encapsulent les entités personne et adresse sont de simples POJO.
Exemple : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 :
Exemple : |
package fr.jmdoudoux.dej.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) |
Le POJO qui encapsule une personne a quelques particularités relatives à la relation avec l'adresse :
Exemple : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.Personne"></mapping>
<mapping class="fr.jmdoudoux.dej.hibernate.Adresse"></mapping>
</session-factory>
</hibernate-configuration>
|
L'application de test est basique :
Exemple : |
package fr.jmdoudoux.dej.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) |
|
La suite de cette section sera développée dans une version future de ce document |
Hibernate propose un support des trois stratégies de base pour le mapping d'héritage de classes :
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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.hibernate.entity.Compte;
import fr.jmdoudoux.dej.hibernate.entity.CompteCourant;
import fr.jmdoudoux.dej.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.
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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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.
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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.entity.CompteCourant"
discriminator-value="CompteCourant">
<property name="decouvert" column="decouvert" type="int"/>
</subclass>
<subclass name="fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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'
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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_
fr.jmdoudoux.dej.hibernate.entity.Compte@1c458a5 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@953079CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.Compte@4cb73c [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@9557173CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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) |
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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.entity.CompteCourant"
table="compte_courant">
<key column="id"/>
<property name="decouvert" column="decouvert" type="int"/>
</joined-subclass>
<joined-subclass name="fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@2473102CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.hibernate.entity.CompteEpargne@21374993CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@5917204CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.hibernate.entity.CompteEpargne@28671960CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
fr.jmdoudoux.dej.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.
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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.hibernate.entity.CompteCourant"
table="compte_courant">
<property name="decouvert" column="decouvert" type="int"/>
</union-subclass>
<union-subclass name="fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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_
fr.jmdoudoux.dej.hibernate.entity.Compte@12a81c9 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@24930042CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.Compte@1a93ff1 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@10814140CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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: fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.hibernate.TestHibernate.main(TestHibernate.java:27)
Exception in thread "main" java.lang.NullPointerException
at fr.jmdoudoux.dej.hibernate.TestHibernate.main(TestHibernate.java:54) |
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 :
Il faut lui ajouter un tag fils <join> pour définir le mapping de la table avec plusieurs propriétés :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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'
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@27737342CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.hibernate.entity.CompteEpargne@31801703CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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'
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.hibernate.entity.Compte@476e01 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@11305489CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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.
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="fr.jmdoudoux.dej.hibernate.entity.Compte"/>
<mapping class="fr.jmdoudoux.dej.hibernate.entity.CompteCourant"/>
<mapping class="fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 + "]";
}
} |
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 fr.jmdoudoux.dej.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: fr.jmdoudoux.dej.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: fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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'
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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_
fr.jmdoudoux.dej.hibernate.entity.Compte@164fc53 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@13942651CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.Compte@6ca594 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@15384254CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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) |
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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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_
fr.jmdoudoux.dej.hibernate.entity.Compte@165d7a2 [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@30230592CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.Compte@129d5bf [id=1, numero=000012345000, solde=0.00]
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@26225329CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.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> |
L'entité de la classe mère utilise la stratégie InheritanceType.JOINED grâce à l'annotation @Inheritance.
Exemple ( code Java 5.0 ) : |
package fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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=?
fr.jmdoudoux.dej.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
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@15462959CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.hibernate.entity.CompteEpargne@12544708CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
fr.jmdoudoux.dej.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 ?
fr.jmdoudoux.dej.hibernate.entity.CompteCourant@9037617CompteCourant [id=2, numero=
000012345010, solde=1200.00, decouvert=2000]
fr.jmdoudoux.dej.hibernate.entity.CompteEpargne@27879211CompteEpargne [ id=3, numero=
000012345020, solde=8000.00, taux=2.10]
fr.jmdoudoux.dej.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> |
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 :
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.
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.
Plusieurs actions peuvent faire réaliser une requête de type SQL par Hibernate :
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 :
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:
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 :
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 :
Ce cache est essentiellement utile pour les requêtes fréquemment exécutées avec les mêmes valeurs de paramètres.
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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> |
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 :
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.
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 :
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.
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.
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 :
Chacun de ces caches possède des caractéristiques et des fonctionnalités :
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 |
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 :
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 :
Le nom de ces deux régions est différent selon la version d'Hibernate utilisée :
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 :
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
[fr.jmdoudoux.dej.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 :
|
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="fr.jmdoudoux.dej.hibernatecache.entity.Pays"
maxElementsInMemory="250" eternal="true" overflowToDisk="false" />
<cache name="fr.jmdoudoux.dej.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
[fr.jmdoudoux.dej.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
[fr.jmdoudoux.dej.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 :
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.
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 :
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.
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.
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.
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.
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] |
Les caches supportés en standard par Hibernate proposent le support d'une ou plusieurs stratégies.
Cache |
read-only |
nonstrict-read-write |
read-write |
transactional |
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 |
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 fr.jmdoudoux.dej.hibernatecache;
import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import fr.jmdoudoux.dej.hibernatecache.entity.Devise;
import fr.jmdoudoux.dej.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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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 :
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="fr.jmdoudoux.dej.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 |
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 fr.jmdoudoux.dej.hibernatecache;
import java.util.Set;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import fr.jmdoudoux.dej.hibernatecache.entity.Devise;
import fr.jmdoudoux.dej.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 :
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="fr.jmdoudoux.dej.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="fr.jmdoudoux.dej.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 :
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="fr.jmdoudoux.dej.hibernatecache.entity.Pays" usage="read-only"/>
<class-cache class="fr.jmdoudoux.dej.hibernatecache.entity.Devise" usage="read-only" />
<collection-cache collection="fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.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.
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 fr.jmdoudoux.dej.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 fr.jmdoudoux.dej.hibernatecache.entity.Devise;
import fr.jmdoudoux.dej.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 :
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 :
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 :
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.
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); |
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.
Les statistiques permettent d'obtenir des informations utiles sur l'utilisation du cache et des sessions :
Hibernate propose des statistiques d'utilisation fournies par la classe SessionFactory. Ces informations sont disponibles de deux manières :
Par défaut, les statistiques sont désactivées. Il y a deux manières de les activer :
Exemple : |
<prop key="hibernate.generate_statistics">true</prop>\n
|
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("fr.jmdoudoux.dej.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("fr.jmdoudoux.dej.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("fr.jmdoudoux.dej.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); |
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 :
Le répertoire tools propose trois outils :
Pour utiliser ces outils, il y a deux solutions possibles :
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 :
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.
|
La suite de ce chapitre sera développée dans une version future de ce document |
|