Hibernate et performance partie 1 : stratégies de chargement

Antonio Gomes Rodrigues

Cet article est la première partie d'une série traitant de la performance avec Hibernate et traite en particulier des stratégies de chargement.

Commentez ♪

- Titre : Hibernate et performance partie 1 : stratégies de chargement

- Auteur : Antonio Gomes Rodrigues

- Parution : 13 septembre 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Gomes Rodrigues Antonio. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Présentation de Hibernate

- III. Problèmes de performances avec Hibernate

- IV. Préparation de l'environnement de test

- IV-A. Création du jeu de données avec Benerator

- IV-B. Mapping et configuration de Hibernate

- V. Préconisations

- VI. Récupérer les informations de fonctionnement de Hibernate

- VI-A. Tracer les requêtes SQL

- VI-B. Tracer la valeur des paramètres hibernate

- VI-C. Tracer les transactions Hibernate

- VI-D. Tracer toute acquisition de ressource JDBC

- VI-E. Statistiques Hibernate

- VI-F. JMX

- VII. Configuration du mapping

- VII-A. Stratégies de chargement

- VII-A-1. Comment (quelle requête SQL est utilisée)

- VII-A-1-a. Chargement par select

- VII-A-1-a-i. Exemple 1 : Récupération du propriétaire d'un costume

- VII-A-1-a-ii. Exemple 2 : Récupération de la liste des costumes d'un consultant

- VII-A-1-b. Chargement par jointure

- VII-A-1-b-i. Exemple 3 : Récupération du propriétaire d'un costume

- VII-A-1-b-ii. Exemple 4 : Récupération de la liste des costumes d'un consultant

- VII-A-1-c. Chargement par lot

- VII-A-1-c-i. Exemple 5 : Récupération du propriétaire pour chaque costume

- VII-A-1-d. Chargement par sous-select

- VII-A-1-d-i. Exemple 6 : Récupération de la liste des costumes par consultant

- VII-A-2. Quand l'association est chargée

- VII-A-2-a. Chargement tardif

- VII-A-2-b. Chargement immédiat

- VII-A-2-c. Chargement « super tardif » d'une collection

- VII-A-2-d. Chargement tardif des attributs

- VII-B. Exemples avec une plus grosse volumétrie

- VII-B-1. Exemple 7 : Chargement par lot

- VII-B-2. Exemple 8 : Chargement par sous-select

- VII-B-3. Exemple 9 : Exemple à très forte volumétrie

- VII-C. Conclusion

- VII-D. Remerciements

- VII-E. Références

I - Introduction

Maintenant que nous avons une méthode d'audit de performance et la possibilité de générer une base de données avec un gros volume, il est temps de regarder d'un peu plus près certaines parties d'un programme Java EE. Dans cette série d'articles, on commencera par la partie Hibernate en nous focalisant sur les performances et en particulier sur les stratégies de chargement pour ce premier article.

II - Présentation de Hibernate

Comme il est dit dans la FAQ, Hibernate est un framework de mapping objet/relationnel qui permet de manipuler les données d'une base de données relationnelle sous forme d'objet.
Pour plus d'informations sur Hibernate, il y a https://java.developpez.com/cours/?page=persistance-cat#hibernate.

III - Problèmes de performances avec Hibernate

De nombreux problèmes de performances peuvent arriver si on ne fait pas attention lors de l'utilisation de Hibernate.
En particulier :

- problèmes dits de N+1 ;

- mauvaise gestion des clés primaires auto-incrémentées ;

- sous utilisation des caches ;

- …

IV - Préparation de l'environnement de test

Afin d'avoir un environnement de test, nous allons le créer nous même à l'aide de Benerator et de Netbeans.

IV-A - Création du jeu de données avec Benerator

Nous allons utiliser Benerator pour générer un jeu de données sur PostreSQL.
Voilà notre schéma de base de données.

Afin d'avoir des traces les plus simples, nous nous contenterons d'un petit volume de données dans un premier temps.
Commençons par créer les tables nécessaires :

drop_tables.sql

DROP SEQUENCE seq_hibernate_id_gen;
DROP TABLE t_costume;
DROP TABLE t_consultant;
DROP TABLE t_couleur;

create_tables.sql

CREATE SEQUENCE seq_hibernate_id_gen start 1;

CREATE TABLE t_couleur (
id_couleur int NOT NULL,
nom_couleur varchar(64) NOT NULL,
PRIMARY KEY  (id_couleur)
);

CREATE TABLE t_consultant (
id_consultant int NOT NULL,
nom varchar(64),
prenom varchar(64),
email varchar(64),
PRIMARY KEY  (id_consultant)
);

CREATE TABLE t_costume (
id_costume int NOT NULL,
couleur_fk int NOT NULL,
consultant_fk int NOT NULL,
prix int NOT NULL,
PRIMARY KEY  (id_costume),
CONSTRAINT t_costume_couleur_fk FOREIGN KEY (couleur_fk) REFERENCES t_couleur (id_couleur),
CONSTRAINT t_costume_consultant_fk FOREIGN KEY (consultant_fk) REFERENCES t_consultant (id_consultant)
);

Puis nous utiliserons un fichier CSV pour importer les couleurs :

t_couleur.import.csv

"id_couleur","nom_couleur",
1,"gris"
2,"bleu"
3,"noir"
4,"marron"
5,"vert"

Maintenant il ne reste plus qu'à générer les données :

hibernatePerf.xml

<?xml version="1.0" encoding="iso-8859-1"?>
<setup     xmlns="http://databene.org/benerator/0.6.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://databene.org/benerator/0.6.3 http://databene.org/benerator-0.6.3.xsd">

<import platforms="db" />

<database id="db" url="jdbc:postgresql://localhost:5432/postgres" driver="org.postgresql.Driver"
schema="public" user="benerator" password="benerator" batch="true" fetchSize="1000"/>

<execute uri="drop_tables.sql" target="db" onError="ignore"/>
<execute uri="create_tables.sql" target="db" optimize="true"/>

<bean id="idGen" spec="new DBSeqHiLoGenerator('seq_hibernate_id_gen', 1, db)" />

<iterate source="t_couleur.import.csv" type="t_couleur" encoding="utf-8" consumer="db" />

<generate type="t_consultant" count="5" consumer="db" pageSize="1000" >
<variable name="individu" generator="org.databene.domain.person.PersonGenerator" dataset="FR" locale="fr"/>
<id name="id_consultant" generator="idGen" />
<attribute name="prenom" script="individu.givenName" />
<attribute name="nom" script="individu.familyName" />
<attribute name="email" script="individu.email" />
</generate>

<generate type="t_costume" count="15" consumer="db" pageSize="1000">
<id name="id_costume" generator="idGen" />
<attribute name="prix" min="100" max="2000" />
<reference name="couleur_fk" targetType="t_couleur" source="db" distribution="random" />
<reference name="consultant_fk" targetType="t_consultant" source="db" distribution="random" cyclic="true" />
</generate>

</setup>

IV-B - Mapping et configuration de Hibernate

De nombreuses solutions existent afin de nous aider dans cette tâche.
Par exemple nous pouvons utiliser Netbeans 6.9 afin de générer les fichiers de mapping et de configuration de Hibernate.
On utilise l'assistant pour la création du fichier de configuration de Hibernate.
Commençons par créer le fichier de configuration et ajouter les librairies Hibernate au projet.

Puis configurons le comportement de l'outil de génération du mapping avec le fichier reveng.xml.

Puis générons les fichiers de mapping.

On ajoute la classe HibernateUtil pour simplifier la gestion de Hibernate.

Ne pas oublier d'ajouter la librairie log4j pour avoir les logs.

V - Préconisations

Mes préconisations sont :

- toujours développer avec Hibernate avec les traces activées et ne pas croire que Hibernate va optimiser les requêtes par magie ;

- initialiser les relations en mode différé/lazzy ;

- puis au cas par cas s'assurer que ce mode de chargement ne génère pas les problèmes dits de N+1 ou les produits cartésiens ;

- optimiser les modes de chargement en utilisant les techniques de chargement par jointure, chargement par sous-select ;

- bien travailler les index, jointures ;

- faire appel à un DBA.

VI - Récupérer les informations de fonctionnement de Hibernate

Avant de commencer l'optimisation de la configuration de Hibernate et de son mapping, il faut bien s'assurer :

- d'avoir un jeu de données en base assez important, car sinon on passera à côté de certains problèmes qui n'apparaissent qu’à forte volumétrie ;

- d'avoir un serveur de bases de données un minimum bien configuré (en particulier sa mémoire cache) et donc faire appel à un DBA ;

- des scénarios réalistes ;

- monitorer tous les serveurs afin que la charge de travaille soit bien répartie.

Une des premières choses à faire est d'activer les logs de Hibernate ou d'utiliser les bons outils afin de savoir ce qu'il se passe. Cela nous permettra de récupérer les requêtes SQL générées pour affiner le tuning de la base de données (ajout d'index, optimisation des tables space…) et de valider le paramétrage de Hibernate.
Regardons comment activer les traces de Hibernate.

VI-A - Tracer les requêtes SQL

La première chose à faire est d'activer la journalisation des requêtes générées.
Cela se fait dans le fichier log4j.properties :

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

log4j.rootLogger=info, stdout

# Trace toutes les requêtes SQL de type DML (gestion des données)
log4j.logger.org.hibernate.SQL=debug
# Trace toutes les requêtes SQL de type DDL (gestion de la structure de la base)
log4j.logger.org.hibernate.tool.hbm2ddl=debug

Ou activer les propriétés hibernate.show_sql et sql_comments dans le fichier hibernate.cfg.xml
Le code suivant :

TCostume costume = (TCostume) session.load(TCostume.class, 4113);
System.out.println(costume.getIdCostume());
System.out.println(costume.getTConsultant().getNom());

Produira les traces suivantes :

select tcostume0_.id_costume as id1_1_0_, tcostume0_.consultant_fk as consultant2_1_0_,
tcostume0_.couleur_fk as couleur3_1_0_, tcostume0_.prix as prix1_0_ from public.t_costume tcostume0_ where tcostume0_.id_costume=?
select tconsultan0_.id_consultant as id1_2_0_, tconsultan0_.nom as nom2_0_, tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_ from public.t_consultant tconsultan0_ where tconsultan0_.id_consultant=?

Afin de formater les requêtes SQL, on pourra activer la propriété hibernate.format_sql dans le fichier hibernate.cfg.xml. Pour cela ajouter :

<property name="hibernate.format_sql">true</property>

Ou utiliser l'assistant de Netbeans.

Et dorénavant, on aura des traces formatées de la manière suivante :

select
tcostume0_.id_costume as id1_1_0_,
tcostume0_.consultant_fk as consultant2_1_0_,
tcostume0_.couleur_fk as couleur3_1_0_,
tcostume0_.prix as prix1_0_
from
public.t_costume tcostume0_
where
tcostume0_.id_costume=?

select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

Et si on veut savoir l'origine de la requête, il suffit d'ajouter dans le fichier hibernate.cfg.xml :

<property name="use_sql_comments">true</property>

Pour obtenir :

/* load hibernatep.TCostume */ select
tcostume0_.id_costume as id1_1_0_,
tcostume0_.consultant_fk as consultant2_1_0_,
tcostume0_.couleur_fk as couleur3_1_0_,
tcostume0_.prix as prix1_0_
from
public.t_costume tcostume0_
where
tcostume0_.id_costume=?

/* load hibernatep.TConsultant */ select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

Ne pas oublier de les désactiver pour les autres tests de performance et pour la mise en production de l'application.

VI-B - Tracer la valeur des paramètres hibernate

Dans le fichier log4j.properties :

log4j.logger.org.hibernate.type=debug

Ce qui nous donnera :

14:11:45,541 DEBUG SQL:401 - select tcustomer0_.id as id0_, tcustomer0_.address_fk as address2_0_, tcustomer0_.login as login0_,
tcustomer0_.password as password0_, tcustomer0_.firstname as firstname0_, tcustomer0_.lastname as lastname0_, tcustomer0_.telephone as telephone0_, tcustomer0_.email as email0_,
tcustomer0_.date_of_birth as date9_0_ from public.t_customer tcustomer0_ where tcustomer0_.id=1002

VI-C - Tracer les transactions Hibernate

Dans le fichier log4j.properties :

log4j.logger.org.hibernate.transaction=debug

Ce qui nous donnera :

DEBUG JDBCTransaction:54 - begin
DEBUG JDBCTransaction:59 - current autocommit status: false
DEBUG SQL:401 - select tcustomer0_.id as id0_, tcustomer0_.address_fk as address2_0_, tcustomer0_.login as login0_, tcustomer0_.password as password0_,
tcustomer0_.firstname as firstname0_, tcustomer0_.lastname as lastname0_, tcustomer0_.telephone as telephone0_, tcustomer0_.email as email0_,
tcustomer0_.date_of_birth as date9_0_ from public.t_customer tcustomer0_ where tcustomer0_.id=1002
1: Paul
DEBUG JDBCTransaction:103 - commit
DEBUG JDBCTransaction:116 - committed JDBC Connection

VI-D - Tracer toute acquisition de ressource JDBC

Dans le fichier log4j.properties :

log4j.logger.org.hibernate.jdbc=debug

Ce qui nous donnera :

DEBUG ConnectionManager:421 - opening JDBC connection
DEBUG AbstractBatcher:366 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
DEBUG SQL:401 - select tcustomer0_.id as id0_, tcustomer0_.address_fk as address2_0_, tcustomer0_.login as login0_, tcustomer0_.password as password0_,
tcustomer0_.firstname as firstname0_, tcustomer0_.lastname as lastname0_, tcustomer0_.telephone as telephone0_, tcustomer0_.email as email0_,
tcustomer0_.date_of_birth as date9_0_ from public.t_customer tcustomer0_ where tcustomer0_.id=1002
DEBUG AbstractBatcher:382 - about to open ResultSet (open ResultSets: 0, globally: 0)
DEBUG AbstractBatcher:389 - about to close ResultSet (open ResultSets: 1, globally: 1)
DEBUG AbstractBatcher:374 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
1: Paul
DEBUG ConnectionManager:404 - aggressively releasing JDBC connection
DEBUG ConnectionManager:441 - releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)]
BUILD SUCCESSFUL (total time: 1 second)

VI-E - Statistiques Hibernate

Si on active hibernate.generate_statistics, Hibernate va fournir un certain nombre de métriques via SessionFactory.getStatistics().
Pour cela, dans le fichier hibernate.cfg.xml. Ajoutons :

<property name="hibernate.generate_statistics">true</property>

Puis dans le code source :

Statistics stats = HibernateUtil.getSessionFactory().getStatistics();

...

stats.logSummary();

donnera :

INFO StatisticsImpl:463 - Logging statistics....
INFO StatisticsImpl:464 - start time: 1280654979840
INFO StatisticsImpl:465 - sessions opened: 1
INFO StatisticsImpl:466 - sessions closed: 1
INFO StatisticsImpl:467 - transactions: 1
INFO StatisticsImpl:468 - successful transactions: 1
INFO StatisticsImpl:469 - optimistic lock failures: 0
INFO StatisticsImpl:470 - flushes: 1
INFO StatisticsImpl:471 - connections obtained: 1
INFO StatisticsImpl:472 - statements prepared: 2
INFO StatisticsImpl:473 - statements closed: 2
INFO StatisticsImpl:474 - second level cache puts: 0
INFO StatisticsImpl:475 - second level cache hits: 0
INFO StatisticsImpl:476 - second level cache misses: 0
INFO StatisticsImpl:477 - entities loaded: 2
INFO StatisticsImpl:478 - entities updated: 0
INFO StatisticsImpl:479 - entities inserted: 0
INFO StatisticsImpl:480 - entities deleted: 0
INFO StatisticsImpl:481 - entities fetched (minimize this): 2
INFO StatisticsImpl:482 - collections loaded: 0
INFO StatisticsImpl:483 - collections updated: 0
INFO StatisticsImpl:484 - collections removed: 0
INFO StatisticsImpl:485 - collections recreated: 0
INFO StatisticsImpl:486 - collections fetched (minimize this): 0
INFO StatisticsImpl:487 - queries executed to database: 0
INFO StatisticsImpl:488 - query cache puts: 0
INFO StatisticsImpl:489 - query cache hits: 0
INFO StatisticsImpl:490 - query cache misses: 0
INFO StatisticsImpl:491 - max query time: 0ms

Si on ne veut pas autant d'informations, on peut spécifier celles que l'on veut.
Par exemple : stats.getQueryCacheMissCount(), stats.getEntityDeleteCount(), stats.getCollectionLoadCount(), stats.getFlushCount()…

VI-F - JMX

Il est bien sûr possible de récupérer un certain nombre d'informations à l'aide de JMX.
Plus d'information sur http://docs.jboss.org/hibernate/core/3.5/reference/fr-FR/html/performance.html#performance-monitoring

VII - Configuration du mapping

Maintenant que nous avons un moyen de mesurer et de comprendre ce qu'il se passe, penchons-nous sur l'affinement du mapping des tables.
Hibernate permet la manipulation des enregistrements sous forme de graphe d'objets, graphe d'objets qu'il faut charger en mémoire avec les risques que cela implique (temps d'exécution couteux, grosse consommation mémoire…). Heureusement, Hibernate propose un certain nombre de stratégies de chargement.

VII-A - Stratégies de chargement

La stratégie de chargement dépendant du contexte (type de requête, paramètres de la requête…) il est plus judicieux de définir des paramètres par défauts dans les fichiers de mapping et de surcharger pour une transaction particulière à l'aide de left join fetch dans les requêtes HQL ou d'utiliser la méthode setFetchMode(FetchMode.JOIN) dans l'API Criteria.
Les stratégies de chargement peuvent être paramétrées à l'aide de paramètres divisés en deux groupes. Le « comment » et le « quand ».

Voyons d'un peu plus près les différentes stratégies de chargement.

VII-A-1 - Comment (quelle requête SQL est utilisée)

VII-A-1-a - Chargement par select

Hibernate récupère les données associées dans un second SELECT.

C'est le comportement par défaut de Hibernate.
Attention le chargement par select est très vulnérable au problème du N+1 selects.

VII-A-1-a-i - Exemple 1 : Récupération du propriétaire d'un costume

Regardons comment récupérer le propriétaire d'un costume :

Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();

TCostume costume = (TCostume) session.load(TCostume.class, 4113);
System.out.println(costume.getIdCostume());

TConsultant proprio = costume.getTConsultant();
System.out.println(proprio.getNom());

session.getTransaction().commit();
session.close();

Génèrera deux requêtes SQL.

select
tcostume0_.id_costume as id1_1_0_,
tcostume0_.consultant_fk as consultant2_1_0_,
tcostume0_.couleur_fk as couleur3_1_0_,
tcostume0_.prix as prix1_0_
from
public.t_costume tcostume0_
where
tcostume0_.id_costume=?

select
tconsultan0_.id_consultant as id1_0_0_,
tconsultan0_.nom as nom0_0_,
tconsultan0_.prenom as prenom0_0_,
tconsultan0_.email as email0_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

La première récupère les informations du costume recherché et la 2e les informations sur le propriétaire.
C'est dommage d'utiliser deux requêtes SQL pour ce résultat alors qu'une seul aurait pu suffire. C'est dans ce cas la que la stratégie de chargement par jointure joue un rôle.

VII-A-1-a-ii - Exemple 2 : Récupération de la liste des costumes d'un consultant

Maintenant on va faire le contraire, on veut la liste des costumes par consultant :

TConsultant consultant = (TConsultant) session.get(TConsultant.class, 4108);
Set sets = consultant.getTCostumes();

for (Iterator iter = sets.iterator(); iter.hasNext();) {
TCostume costume = (TCostume) iter.next();
System.out.println(costume.getIdCostume());
}

Génèrera deux requêtes SQL (une pour récupérer l'identifiant du costume associé au consultant avec l'identifiant 4108, puis une pour récupérer les informations de la table costume). C'est le problème dit de N+1

select
tconsultan0_.id_consultant as id1_0_,
tconsultan0_.nom as nom0_,
tconsultan0_.prenom as prenom0_,
tconsultan0_.email as email0_
from
public.t_consultant tconsultan0_

select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk=?

VII-A-1-b - Chargement par jointure

Hibernate récupère les données associées dans un même SELECT à l'aide d'un OUTER JOIN.

Pour utiliser cette stratégie, on a trois choix :

- utiliser fetch=« join » dans l'association (dans le fichier de mapping) ;

- utiliser left join fetch dans les requêtes HQL ;

- utiliser la méthode setFetchMode(FetchMode.JOIN) dans les criteria.

Il faudra faire attention à se retrouver avec des requêtes trop complexes comportant trop de jointures.

VII-A-1-b-i - Exemple 3 : Récupération du propriétaire d'un costume

Pour sélectionner cette stratégie, on peut positionner à join le paramètre fetch dans le fichier de mapping de TCostume :

<many-to-one name="TConsultant" class="hibernateperfarticle.TConsultant" fetch="join">
<column name="consultant_fk" not-null="true" />
</many-to-one>

Le même code que précédemment :

TCostume costume = (TCostume) session.load(TCostume.class, 4113);
System.out.println(costume.getIdCostume());

TConsultant proprio = costume.getTConsultant();
System.out.println(proprio.getNom());

Donnera cette fois une seule requête :

select
tcostume0_.id_costume as id1_1_1_,
tcostume0_.consultant_fk as consultant2_1_1_,
tcostume0_.couleur_fk as couleur3_1_1_,
tcostume0_.prix as prix1_1_,
tconsultan1_.id_consultant as id1_0_0_,
tconsultan1_.nom as nom0_0_,
tconsultan1_.prenom as prenom0_0_,
tconsultan1_.email as email0_0_
from
public.t_costume tcostume0_
inner join
public.t_consultant tconsultan1_
on tcostume0_.consultant_fk=tconsultan1_.id_consultant
where
tcostume0_.id_costume=?

Donc si on sait que l'on va utiliser les informations des tables jointes, l'utilisation de la stratégie par jointure est une bonne idée.
Mais comme cela dépend des cas, il est plus judicieux de surcharger pour une transaction particulière à l'aide de left join fetch dans les requêtes HQL.

Query query = session.createQuery("from TCostume where idCostume=4113");
Iterator it = query.list().iterator();
while (it.hasNext()) {
TCostume costume = (TCostume) it.next();
System.out.println(costume.getIdCostume() + " : " + costume.getTConsultant().getNom());
}

produira :

select
tcostume0_.id_costume as id1_1_,
tcostume0_.consultant_fk as consultant2_1_,
tcostume0_.couleur_fk as couleur3_1_,
tcostume0_.prix as prix1_
from
public.t_costume tcostume0_
where
tcostume0_.id_costume=4113

select
tconsultan0_.id_consultant as id1_0_0_,
tconsultan0_.nom as nom0_0_,
tconsultan0_.prenom as prenom0_0_,
tconsultan0_.email as email0_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

Alors que si on surcharge la stratégie de chargement :

Query query = session.createQuery("from TCostume cos left join fetch cos.TConsultant where cos.idCostume=4113");
Iterator it = query.list().iterator();
while (it.hasNext()) {
TCostume costume = (TCostume) it.next();
System.out.println(costume.getIdCostume() + " : " + costume.getTConsultant().getNom());
}

on aura ;

select
tcostume0_.id_costume as id1_1_0_,
tconsultan1_.id_consultant as id1_0_1_,
tcostume0_.consultant_fk as consultant2_1_0_,
tcostume0_.couleur_fk as couleur3_1_0_,
tcostume0_.prix as prix1_0_,
tconsultan1_.nom as nom0_1_,
tconsultan1_.prenom as prenom0_1_,
tconsultan1_.email as email0_1_
from
public.t_costume tcostume0_
left outer join
public.t_consultant tconsultan1_
on tcostume0_.consultant_fk=tconsultan1_.id_consultant
where
tcostume0_.id_costume=4113

De même pour l'API Criteria il faudra utiliser la méthode setFetchMode(FetchMode.JOIN).

VII-A-1-b-ii - Exemple 4 : Récupération de la liste des costumes d'un consultant

On reprend le même code que précédemment pour récupérer la liste des costumes par consultant :

TConsultant consultant = (TConsultant) session.get(TConsultant.class, 4108);
Set sets = consultant.getTCostumes();

for (Iterator iter = sets.iterator(); iter.hasNext();) {
TCostume costume = (TCostume) iter.next();
System.out.println(costume.getIdCostume());
}

On active le chargement par jointure dans le fichier Tconsultant.hbm.xml :

<set name="TCostumes" inverse="true" fetch="join">
<key>
<column name="consultant_fk" not-null="true" />
</key>
<one-to-many class="hibernateperfarticle.TCostume"/>
</set>

Et cette fois-ci, on n'a plus qu'une seule requête SQL au lieu de deux :

select
tconsultan0_.id_consultant as id1_0_1_,
tconsultan0_.nom as nom0_1_,
tconsultan0_.prenom as prenom0_1_,
tconsultan0_.email as email0_1_,
tcostumes1_.consultant_fk as consultant2_3_,
tcostumes1_.id_costume as id1_3_,
tcostumes1_.id_costume as id1_1_0_,
tcostumes1_.consultant_fk as consultant2_1_0_,
tcostumes1_.couleur_fk as couleur3_1_0_,
tcostumes1_.prix as prix1_0_
from
public.t_consultant tconsultan0_
left outer join
public.t_costume tcostumes1_
on tconsultan0_.id_consultant=tcostumes1_.consultant_fk
where
tconsultan0_.id_consultant=?

Le même résultat sera obtenu avec l'API Criteria :

TConsultant consultant = (TConsultant) session.createCriteria(TConsultant.class)
.add(Restrictions.eq("idConsultant", Integer.valueOf(4108)))
.setFetchMode("TCostumes", FetchMode.JOIN).uniqueResult();
Set sets = consultant.getTCostumes();

for (Iterator iter = sets.iterator(); iter.hasNext();) {
TCostume costume = (TCostume) iter.next();
System.out.println(costume.getIdCostume());
}

VII-A-1-c - Chargement par lot

Hibernate récupère un lot d'instances en un seul SELECT en spécifiant une liste de clé primaire ou de clé étrangère.

Le paramètre batch-size peut se positionner au niveau de la classe comme vu précédemment ou au niveau des collections.

VII-A-1-c-i - Exemple 5 : Récupération du propriétaire pour chaque costume

Prenons un exemple pour voir son utilité :

Query query = session.createQuery("from TCostume");
Iterator it = query.list().iterator();
while (it.hasNext()) {
TCostume costume = (TCostume) it.next();
System.out.println(costume.getIdCostume() + " : " + costume.getTConsultant().getNom());
}

On aura six requêtes SQL (une requête qui récupère la liste des costumes et cinq autres pour récupérer les noms des consultants) :

select
tcostume0_.id_costume as id1_1_,
tcostume0_.consultant_fk as consultant2_1_,
tcostume0_.couleur_fk as couleur3_1_,
tcostume0_.prix as prix1_
from
public.t_costume tcostume0_

select
tconsultan0_.id_consultant as id1_0_0_,
tconsultan0_.nom as nom0_0_,
tconsultan0_.prenom as prenom0_0_,
tconsultan0_.email as email0_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

Maintenant modifions le fichier de mapping de TConsultant afin d'utiliser le chargement par lot à l'aide du paramètre batch-size :

<class name="hibernateperfarticle.TConsultant" batch-size="10" table="t_consultant" schema="public">

Et nous obtenons seulement deux requêtes (une requête qui récupère la liste des costumes  et une pour récupérer tous les consultants).

select
tcostume0_.id_costume as id1_1_,
tcostume0_.consultant_fk as consultant2_1_,
tcostume0_.couleur_fk as couleur3_1_,
tcostume0_.prix as prix1_
from
public.t_costume tcostume0_

select
tconsultan0_.id_consultant as id1_0_0_,
tconsultan0_.nom as nom0_0_,
tconsultan0_.prenom as prenom0_0_,
tconsultan0_.email as email0_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant in (
?, ?, ?, ?, ?
)

VII-A-1-d - Chargement par sous-select

Hibernate récupère les associations pour toutes les entités récupérées dans une requête dans un second SELECT.

De même que pour le chargement par jointure, il faut faire attention à ne pas se retrouver avec une requête SQL trop compliquée.

VII-A-1-d-i - Exemple 6 : Récupération de la liste des costumes par consultant

Toujours avec le même code afin de récupérer la liste des costumes par consultant :

List<TConsultant> list = session.createQuery("from TConsultant").list();

for (TConsultant consultants : list) {

Set sets = consultants.getTCostumes();

for (Iterator iter = sets.iterator(); iter.hasNext();) {
TCostume costume = (TCostume) iter.next();
System.out.println(consultants.getNom()+" "+costume.getIdCostume());

}
}

On aura 6 requêtes SQL (une pour récupérer la liste des consultants, puis une par consultant afin de récupérer ses costumes) :

select
tconsultan0_.id_consultant as id1_0_,
tconsultan0_.nom as nom0_,
tconsultan0_.prenom as prenom0_,
tconsultan0_.email as email0_
from
public.t_consultant tconsultan0_

select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk=?

Maintenant activons le chargement par sous-select dans le fichier TConsultant.hbm.xml :

<set name="TCostumes" inverse="true" fetch="subselect">

Il ne reste plus que deux requêtes SQL :

select
tconsultan0_.id_consultant as id1_0_,
tconsultan0_.nom as nom0_,
tconsultan0_.prenom as prenom0_,
tconsultan0_.email as email0_
from
public.t_consultant tconsultan0_

select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk in (
select
tconsultan0_.id_consultant
from
public.t_consultant tconsultan0_
)

VII-A-2 - Quand l'association est chargée

VII-A-2-a - Chargement tardif

Une collection est chargée lorsque l'application invoque une méthode sur cette collection (il s'agit du mode de chargement par défaut pour les collections).
Reprenons le même exemple que précédemment :

TConsultant consultant = (TConsultant) session.get(TConsultant.class, 4108);
Set sets = consultant.getTCostumes();

for (Iterator iter = sets.iterator(); iter.hasNext();) {
TCostume costume = (TCostume) iter.next();
System.out.println(costume.getIdCostume());
}

Activons les bons logs.
Comme il est indiqué dans les logs, on ne charge dans un premier temps que les informations du consultant :

/* load hibernatep.TConsultant */
select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

Puis dans un 2e temps seulement, les informations de ses costumes :

/* load one-to-many hibernatep.TConsultant.TCostumes */
select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk=?

Mettons un point d'arrêt avant la récupération des costumes et lançons le programme en mode debug.

Comme on peut le voir, la 2e requête SQL n'est pas exécutée.
Puis lorsqu'on a besoin des informations des costumes, la requête SQL est exécutée.

VII-A-2-b - Chargement immédiat

Une association, une collection ou un attribut est chargé immédiatement lorsque l'objet auquel appartient cet élément est chargé.
Refaisons la même chose que précédemment, mais en ajoutant le paramètre lazy=« false » à l'association concernée :

<set name="TCostumes" inverse="true" lazy="false">

Et là, on voit bien que les deux requêtes sont exécutées directement.

Afin de bien le confirmer, on exécute le code suivant tout seul en commentant les autres lignes :

TConsultant consultant = (TConsultant) session.get(TConsultant.class, 4108);

On obtient bien les deux mêmes requêtes.
Donc si on n'a pas besoin des détails des costumes, le chargement immédiat est pénalisant, car il va exécuter une requête qui ne sert à rien.
Par contre dans le cas contraire, cela revient au même, car on aura les deux mêmes requêtes SQL. Si on ne veut qu'une seule requête qui récupère toutes les informations, il faut utiliser le chargement par jointure comme vu précédemment.
Donc le chargement immédiat est à utiliser avec précaution, car on peut se retrouver avec énormément de requêtes SQL exécutées alors que seule la 1re est utile.
On verra plus loin avec une volumétrie plus importante que cela peut poser de gros problème. Mais avant cela, ajoutons un chargement immédiat entre TCouleur et TCostumes et exécutons le même code que celui de la partie « chargement tardif ».

<many-to-one name="TCouleur" class="hibernatep.TCouleur" fetch="select"  lazy="false">

Cette fois-ci on obtient beaucoup plus de requêtes SQL, car pour chaque costume on récupère sa couleur :

/* load hibernatep.TConsultant */ select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

/* load one-to-many hibernatep.TConsultant.TCostumes */ select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk=?

/* load hibernatep.TCouleur */ select
tcouleur0_.id_couleur as id1_0_0_,
tcouleur0_.nom_couleur as nom2_0_0_
from
public.t_couleur tcouleur0_
where
tcouleur0_.id_couleur=?

/* load hibernatep.TCouleur */ select
tcouleur0_.id_couleur as id1_0_0_,
tcouleur0_.nom_couleur as nom2_0_0_
from
public.t_couleur tcouleur0_
where
tcouleur0_.id_couleur=?

/* load hibernatep.TCouleur */ select
tcouleur0_.id_couleur as id1_0_0_,
tcouleur0_.nom_couleur as nom2_0_0_
from
public.t_couleur tcouleur0_
where
tcouleur0_.id_couleur=?

Je vous laisse imaginer sur une forte volumétrie.

VII-A-2-c - Chargement « super tardif » d'une collection

Comme pour le chargement tardif, mais en plus intelligent, car les éléments de la collection sont récupérés individuellement depuis la base de données lorsque cela est nécessaire.  De plus certaines fonctions comme size(), contains(), get()… ne déclencheront pas de requête SQL supplémentaire.
La différence de performance ne se verra que sur les très grosses collections et/ou lors de l'utilisation d'une des fonctions précédentes.
Le chargement « super tardif » se paramètre avec lazy=« extra ».
Prenons comme exemple ce code :

TConsultant consultant = (TConsultant) session.get(TConsultant.class, 4108);
System.out.println(consultant.getTCostumes().size());

Avec un chargement tardif, on aura deux requêtes SQL :

/* load hibernatep.TConsultant */ select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

/* load one-to-many hibernatep.TConsultant.TCostumes */ select
tcostumes0_.consultant_fk as consultant2_1_,
tcostumes0_.id_costume as id1_1_,
tcostumes0_.id_costume as id1_1_0_,
tcostumes0_.consultant_fk as consultant2_1_0_,
tcostumes0_.couleur_fk as couleur3_1_0_,
tcostumes0_.prix as prix1_0_
from
public.t_costume tcostumes0_
where
tcostumes0_.consultant_fk=?

Une fois en mode « super tardif », on aura toujours deux requêtes SQL, mais la 2e sera plus « légère » :

<set name="TCostumes" inverse="true" lazy="extra">

/* load hibernatep.TConsultant */ select
tconsultan0_.id_consultant as id1_2_0_,
tconsultan0_.nom as nom2_0_,
tconsultan0_.prenom as prenom2_0_,
tconsultan0_.email as email2_0_
from
public.t_consultant tconsultan0_
where
tconsultan0_.id_consultant=?

select
count(id_costume)
from
public.t_costume
where
consultant_fk =?

VII-A-2-d - Chargement tardif des attributs

C'est la même chose que le chargement tardif, mais pour un attribut. Cela peut être utile si une classe persistante contient énormément de propriétés ou quelques propriétés volumineuses (un long texte…).
Pour l'utiliser il faut ajouter lazy=« true » à un attribut et d'activer l'instrumentation du bytecode par Hibernate.
Une autre solution est d'utiliser les projections pour ne charger que les attributs que l'on veut.
Par exemple avec l'API Criteria, pour ne charger que l'identifiant et le nom des consultants :

List listeId = session.createCriteria(TConsultant.class).
setProjection( Projections.projectionList()
.add(Projections.property("idConsultant"))
.add(Projections.property("nom")))
.list();

donnera en SQL :

/* criteria query */ select
this_.id_consultant as y0_,
this_.nom as y1_
from
public.t_consultant this_

La même chose est possible en HQL :

Query query = session.createQuery("select idConsultant,nom from TConsultant");

VII-B - Exemples avec une plus grosse volumétrie

Maintenant reprenons deux exemples précédemment étudiés, mais cette fois-ci avec une volumétrie plus importante afin d’observer le gain potentiel d'un bon paramétrage d'Hibernate.
Pour changer de volumétrie, il suffit de changer la valeur des paramètres « count » dans notre fichier projet de Benerator.
Pour avoir 1000 de consultants :

<generate type="t_consultant" count="1000" consumer="db" pageSize="1000" >

Et 3000 de costumes :

<generate type="t_costume" count="3000" consumer="db" pageSize="1000">

On se retrouve assez rapidement avec une base de données rempli.

VII-B-1 - Exemple 7 : Chargement par lot

Reprenons l'exemple utilisé lors de la partie « Chargement par lot » et exécutons-le avec les paramètres par défaut de Hibernate (chargement par select et tardif).
Comme prévu, on se retrouve avec 1001 requêtes SQL.
En modifiant le fichier de mapping de TConsultant afin d'utiliser le chargement par lot à l'aide du paramètre batch-size, il ne reste plus que 11 requêtes SQL :

<class name="hibernateperfarticle.TConsultant" batch-size="100" table="t_consultant" schema="public">

VII-B-2 - Exemple 8 : Chargement par sous-select

Maintenant ré exécutons l'exemple utilisé lors de la partie « Chargement par sous-select » et exécutons-le avec les paramètres par défaut de Hibernate (chargement par select et tardif).
Comme prévu, on se retrouve avec 1001 requêtes SQL.
Maintenant activons le chargement par sous-select dans le fichier TConsultant.hbm.xml afin de n'avoir plus que deux requêtes SQL :

<set name="TCostumes" inverse="true" fetch="subselect">

VII-B-3 - Exemple 9 : Exemple à très forte volumétrie

Pour les plus courageux et ceux qui veulent prendre de l'avance sur l'un des prochains articles sur Hibernate, ils peuvent essayer avec cette volumétrie.
1 000 000 de consultants :

<generate type="t_consultant" count="1000000" consumer="db" pageSize="1000" >

5 000 000 de costumes :

<generate type="t_costume" count="3000000" consumer="db" pageSize="1000">

Attention, il faut laisser un peu de temps à Benerator pour générer cette volumétrie (sur ma machine, il génère environ 4 600 000 lignes par heure).

VII-C - Conclusion

Comme on a pu le voir, un paramétrage plus fin des stratégies de chargement d'Hibernate permet d'optimiser notre application en réduisant le nombre de requêtes SQL générées.
Mais il faut faire attention, car réduire le nombre de requêtes SQL n'est pas toujours la meilleure solution si on se retrouve avec des requêtes trop complexes et consommatrices. En effet la 2e piste d'optimisation est la gestion des caches de Hibernate qu'il faudra combiner avec la gestion des stratégies de chargement. C'est ce que nous verrons lors du prochain article consacré à Hibernate.

VII-D - Remerciements

Merci à ram-0000 pour sa relecture orthographique.

VII-E - Références

Documentation officielle : http://www.hibernate.org/docs.html
Documentation sur dvp.com : https://java.developpez.com/cours/?page=persistance-cat#hibernate
Site officiel de Benerator : http://databene.org/databene-benerator