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 http://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.
Voila notre schéma de base de données.

Image non disponible

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
Sélectionnez
DROP SEQUENCE seq_hibernate_id_gen;
DROP TABLE t_costume;
DROP TABLE t_consultant;
DROP TABLE t_couleur;


create_tables.sql
Sélectionnez
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
Sélectionnez
"id_couleur","nom_couleur",
1,"gris"
2,"bleu"
3,"noir"
4,"marron"
5,"vert"


Maintenant il ne reste plus qu'a générer les données.

hibernatePerf.xml
Sélectionnez
<?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 tache.
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.

Image non disponible

Image non disponible

Image non disponible

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

Image non disponible

Image non disponible

Image non disponible

Puis générons les fichiers de mapping.

Image non disponible

Image non disponible

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

Image non disponible

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 à coté de certains problèmes qui n'apparaissent que à 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.

 
Sélectionnez
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.

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

Produira les traces suivantes.

 
Sélectionnez
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.

 
Sélectionnez
<property name="hibernate.format_sql">true</property>

Ou utiliser l'assistant de Netbeans.

Image non disponible

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

 
Sélectionnez
    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

 
Sélectionnez
<property name="use_sql_comments">true</property>

Pour obtenir.

 
Sélectionnez
    /* 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 performances et pour la mise en production de l'application.

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

Dans le fichier log4j.properties.

 
Sélectionnez
log4j.logger.org.hibernate.type=debug

Ce qui nous donnera.

 
Sélectionnez
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.

 
Sélectionnez
log4j.logger.org.hibernate.transaction=debug

Ce qui nous donnera.

 
Sélectionnez
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.

 
Sélectionnez
log4j.logger.org.hibernate.jdbc=debug

Ce qui nous donnera.

 
Sélectionnez
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 certains nombre de métriques via SessionFactory.getStatistics().
Pour cela, dans le fichier hibernate.cfg.xml. Ajoutons.

 
Sélectionnez
<property name="hibernate.generate_statistics">true</property>

Puis dans le code source.

 
Sélectionnez
Statistics stats = HibernateUtil.getSessionFactory().getStatistics();

...

stats.logSummary();

Donnera.

 
Sélectionnez
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 sur 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é en 2 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ée 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.

 
Sélectionnez
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 2 requêtes SQL.

Image non disponible

 
Sélectionnez
    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 2ème les informations sur le propriétaire.
C'est dommage d'utiliser 2 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.  

 
Sélectionnez
        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 2 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 dits de N+1

Image non disponible

 
Sélectionnez
    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ée dans un même SELECT à l'aide d'un OUTER JOIN.

Pour utiliser cette stratégie, on a 3 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 jointure.

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

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

 
Sélectionnez
        <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.

 
Sélectionnez
        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.

Image non disponible

 
Sélectionnez
    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.

 
Sélectionnez
        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.

 
Sélectionnez
    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.

 
Sélectionnez
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.

Image non disponible
 
Sélectionnez
    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.

 
Sélectionnez
        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.

 
Sélectionnez
        <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 2

Image non disponible

 
Sélectionnez
    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.

 
Sélectionnez
        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é.

 
Sélectionnez
        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 6 requêtes SQL (une requête qui récupère la liste des costumes et 5 autres pour récupérer les noms des consultants).

 
Sélectionnez
    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.

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

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

Image non disponible

 
Sélectionnez
    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é.

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.

 
Sélectionnez
        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)

 
Sélectionnez
    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

 
Sélectionnez
        <set name="TCostumes" inverse="true" fetch="subselect">

Il ne reste plus que 2 requêtes SQL.

Image non disponible

 
Sélectionnez
    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.

 
Sélectionnez
        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.

 
Sélectionnez
    /* 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 2ème temps seulement, les informations de ses costumes.

 
Sélectionnez
    /* 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..

Image non disponible

Comme on peut le voir, la 2ème 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é.

Image non disponible

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.

 
Sélectionnez
<set name="TCostumes" inverse="true" lazy="false">

Et la on voit bien que les 2 requêtes sont exécutées directement.

Image non disponible

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

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

On obtient bien les 2 mêmes requêtes.
Donc si on n'a pas besoins 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 2 même 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 peu se retrouver avec énormément de requêtes SQL exécuté alors que seul la 1ère 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".

 
Sélectionnez
<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.

 
Sélectionnez
    /* 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.

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

Avec un chargement tardif, on aura 2 requêtes SQL

 
Sélectionnez
    /* 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 2 requêtes SQL mais la 2ème sera plus "légère".

 
Sélectionnez
<set name="TCostumes" inverse="true" lazy="extra">
 
Sélectionnez
    /* 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.

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

Donnera en SQL

 
Sélectionnez
    /* 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.

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

VIII. Exemples avec une plus grosse volumétrie

Maintenant reprenons 2 exemples précédemment étudié mais cette fois ci avec une volumétrie plus importante afin de 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 1 000 de consultants.

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

Et 3 000 de costumes.

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

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

VIII-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.

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

VIII-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 2 requêtes SQL.

 
Sélectionnez
<set name="TCostumes" inverse="true" fetch="subselect">

VIII-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.

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

5 000 000 de costumes.

 
Sélectionnez
<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).

IX. 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 meilleur solution si on se retrouve avec des requêtes trop complexes et consommatrices. En effet la 2ème 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.

X. Remerciements

Merci à ram-0000 pour sa relecture orthographique

XI. Références