Hibernate en quelques mots

Hibernate est un moteur de mapping object-relationnel (ORM) qui permet de charger des données venant d’une base de données, vers le monde Java en réduisant la quantité de code Java à écrire.
Vous définissez une classe Java, ensuite un fichier de mapping au format XML, et Hibernate offre alors un moyen pour charger, modifier et effacer vos données. Il dispose aussi d’un cache de requête afin d’optimiser les appels et d’éviter des aller-retours inutiles avec la base de données. Un cache de second niveau offre un moyen très puissant mais délicat à maîtriser pour partager vos données et réduire le coût de la base de données dans une application.
Il est possible d’utiliser soit des annotations sur la classe Java, soit des fichiers XML. Sachez que si vous utilisez des annotations, un fichier de configuration XML peut vous permettre de surcharger le mapping des annotations, et donc d’éviter d’être bloqué par du code déjà compilé sur lequel vous n’avez pas le contrôle.

Objectifs de l’article

La première partie de l’article est assez dictatique pour ceux qui ne connaissent pas Hibernate 3. La seconde partie sera plus intéressante pour les utilisateurs qui souhaitent voir quelques options avancées d’Hibernate. Nous verrons comment gérer efficacement les relations entre les objets, les stratégies de chargement et les différentes types d’option de chargement tardif (Lazy instanciation).

Code source de l’article

Pour les besoins de cet article j’ai préparé un projet simple avec maven2, que vous pouvez télécharger en fin d’article. Le code est distribué sous licence Creatives Commons v2 avec quelques restrictions sur la non modification et l’interdiction d’utiliser le code à des fins commerciales sans mon accord.

Si vous souhaitez tester quelques fonctionnalités en lisant l’article, vous aurez besoin de Java 5 et de Maven 2.0.6 minimum pour compiler et tester le code. J’utilise un test unitaire JUnit 3.8 simple pour lancer les différents tests, un simple « mvn test » en ligne de commande vous permet de compiler le code et de lancer la base de données embarquée HSQLDB.

Un peu de relationnel

J’ai repris un exemple simple pour passer en revue quelques caractéristiques d’Hibernate plus ou moins connues.
Prenons tout d’abord la classe Item, correspondant à un article.
Un Item est caractérisé par son nom, son prix et une clé d’identité. Un constructeur vide sans argument est requis pour qu’Hibernate puisse décorer cette classe avec un proxy avec la librairie CGLIB, sans quoi une exception org.hibernate.InstantiationException sera levée par Hibernate. De même il est recommandé de ne pas déclarer de classe finale comme entité si vous souhaitez qu’Hibernate puisse utiliser un proxy. Si malgré tout vous êtes obligés de déclarer une méthode finale, il faudra alors désactiver le chargement tardif (lazy=false) dans la configuration de l’entité [1]

public class Item {
    private Integer id;
    private String name;
    private float price;
    private Set<Bid> bids=new HashSet<Bid>();

    public Item() {

    }

    public Item(String name, float price){
        //...
    }
     // Getter, setter, equals, hashCode et toString
}

La classe Bid (enchère en anglais) est une offre émise par un acheteur sur notre Item. Les attributs sont l’heure de l’enchère et son montant.

public class Bid {
    private Integer id;
    private float amount;
    private Date date;
    private Item item;

    public Bid(){
    }

    // getter, setter, hashCode, equals
}

Le fichier de mapping Hibernate Item.hbm.xml permet à Hibernate d’effectuer le mapping entre la classe Item et notre base de données. La relation un-vers-plusieurs (un Item contient plusieurs Bids) est définie avec un set. Nous utiliserons une table de jointure ITEM_BIDS, le chargement tardif sera désactivé (lazy=false), nous indiquons à Hibernate que la relation inverse est déclarée dans Bid (inverse=true).

    <hibernate-mapping>

    <class name="org.touilleur.hibernate.v1.Item" table="ITEM">
        <id name="id" column="ITEM_ID">
            <generator class="native"/>
        </id>
        <property name="name"/>
        <property name="price"/>
        <set name="bids" table="ITEM_BIDS"
                                       lazy="false"
                                       inverse="true"
                                       fetch="select"
                                       cascade="all">
            <key column="ITEM_ID" not-null="true"/>
            <one-to-many class="org.touilleur.hibernate.v1.Bid"/>
        </set>
    </class>

</hibernate-mapping>

Hibernate recommande d’utiliser des associations symétriques. Dans notre application, une enchère sans article n’a pas de sens. Nous configurons donc notre association many-to-one avec l’attribut not-null à true dans le fichier Bid.hbm.xml:

<hibernate-mapping>
    <class name="org.touilleur.hibernate.v1.Bid" table="BID">
        <id name="id" column="BID_ID">
            <generator class="native"/>
        </id>
        <property name="amount"/>
        <property name="date"/>
        <many-to-one name="item"
                               column="ITEM_ID"
                               class="org.touilleur.hibernate.v1.Item"
                               not-null="true"/>
    </class>

</hibernate-mapping>

Stratégie de chargement de l’association

Hibernate évite au maximum de faire appel à la base de données, et tend à limiter le nombre de requête, afin d’alléger le traitement. Par défaut la stratégie de chargement tardive qui permet d’optimiser les performances, peut provoquer quelques moments difficile lorsque surgit l’exception LazyInitializationException.

Une exception de type LazyInitializationException sera renvoyée par Hibernate si une collection ou un proxy non initialisé est accédé en dehors de la portée de la Session, e.g. lorsque l’entité à laquelle appartient la collection ou qui a une référence vers le proxy est dans l’état « détachée ».

Cela se produit dans les architectures 3-tiers, où le tiers de présentation Web est séparé physiquement de l’ejbtiers pour des raisons de sécurité. Ce type d’architecture soufre de plusieurs soucis : si vous interdisez l’accès à la base de données à partir du web-tiers, il faudra donc que la stratégie de chargement tardive d’Hibernate soit désactivée (lazy=false). Dans le cas où nous souhaitons charger un Item, toutes les enchères associées seront alors chargées. Imaginez que pour seulement afficher le nom de l’Item, nous allons aussi charger 1500 Bids par exemple…
Il est alors important de configurer Hibernate finement et d’utiliser des jointures afin d’éviter trop de requêtes.

Dans un premier temps, nous allons regarder les différentes possibilités de chargement d’une association, en essayant différentes valeurs pour l’attribut fetch de l’attribut set. La valeur par défaut lorsqu’elle n’est pas précisée est « select ». Les 3 valeurs possibles sont select, join et subselect. Nous désactivons le chargement tardif en ajoutant le mot clé « lazy=false » dans le fichier Item.hbm.xml.

Fetch strategy par défaut en mode lazy=false

Pour ce test, dans le test unitaire SelectTest, nous allons voir comment il est possible de changer la stratégie de chargement directement dans le code. Avant cela, regardons comment travaille Hibernate par défaut avant de tester d’autres modes de chargement. Par défault lorsque FetchMode est à DEFAULT comme ci-dessous, Hibernate se repose sur ce que vous avez déclaré dans votre fichier de mapping, Item.hbm.xml pour nous.

// voir org.touilleur.hibernate.SelectTest
 Item item1 = (Item) session.createCriteria(Item.class)
                .setFetchMode("bids", FetchMode.DEFAULT)
                .add(Restrictions.idEq(id))
                .uniqueResult();

La configuration du chargement de l’association s’effectue dans le fichier de configuration hibernate.cfg.xml à l’aide de l’attribut fetch. La valeur par défaut est select, les 2 autres valeurs possibles sont join et subselect. Voyons d’abord par défaut le comportement d’Hibernate :

       <set name="bids" table="ITEM_BIDS" lazy="false" inverse="false"
                        fetch="select" cascade="all">

Pour continuer, exécutez la méthode testSelectOneItemWithDefault.
La console affiche les requêtes SQL suivantes:
...
INFO 2009-02-27 22:24:21,788 [Demo Hibernate] : Starting selectOneItemWithDefault
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_0_, this_.name as name0_0_,
this_.price as price0_0_ from ITEM this_ where this_.ITEM_ID = ?
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_,
bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_,
bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
INFO 2009-02-27 22:24:21,791 [Demo Hibernate] : Loaded item1 with a FetchMode set to DEFAULT...

2 requêtes sont nécessaires pour effectuer le chargement de l’unique Item.

Chargement de l’ensemble des Items

Le test unitaire testSelecTAllItemsWithDefaultFetchMode sélectionne les 2 Items de notre base et affiche ensuite les 2 items. La méthode toString de la class Item force le chargement de la liste des enchères. Nous avons désactivé le chargement tardif (lazy=false) et il est donc normal de voir Hibernate charger les Bids.

Que se passe-t-il lorsque nous exécutons ce code ?

INFO 2009-03-03 10:18:30,670 [Demo Hibernate] : testSelecTAllItemsWithDefaultFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_0_, this_.name as name0_0_, this_.price as price0_0_ from ITEM this_
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
INFO 2009-03-03 10:18:30,704 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=7, amount=107.0, date=2009-03-03 10:18:30.637}, Bid{id=10, amount=110.0, date=2009-03-03 10:18:30.643}, Bid{id=6, amount=106.0, date=2009-03-03 10:18:30.632}, Bid{id=8, amount=108.0, date=2009-03-03 10:18:30.638}, Bid{id=2, amount=102.0, date=2009-03-03 10:18:30.626}, Bid{id=4, amount=104.0, date=2009-03-03 10:18:30.629}, Bid{id=1, amount=101.0, date=2009-03-03 10:18:30.625}, Bid{id=9, amount=109.0, date=2009-03-03 10:18:30.639}, Bid{id=5, amount=105.0, date=2009-03-03 10:18:30.63}, Bid{id=3, amount=103.0, date=2009-03-03 10:18:30.628}], name='TV LCD', price=340.0}
INFO 2009-03-03 10:18:30,706 [Demo Hibernate] : Item #2 : Item{id=2, bids=[Bid{id=20, amount=144.0, date=2009-03-03 10:18:30.657}, Bid{id=15, amount=125.0, date=2009-03-03 10:18:30.651}, Bid{id=13, amount=43.0, date=2009-03-03 10:18:30.647}, Bid{id=16, amount=116.0, date=2009-03-03 10:18:30.652}, Bid{id=14, amount=84.0, date=2009-03-03 10:18:30.648}, Bid{id=11, amount=10.0, date=2009-03-03 10:18:30.645}, Bid{id=17, amount=137.0, date=2009-03-03 10:18:30.654}, Bid{id=19, amount=139.0, date=2009-03-03 10:18:30.656}, Bid{id=12, amount=22.0, date=2009-03-03 10:18:30.646}, Bid{id=18, amount=138.0, date=2009-03-03 10:18:30.655}], name='Plasma', price=1230.0}
INFO 2009-03-03 10:18:30,709 [Demo Hibernate] : testSelecTAllItemsWithDefaultFetchMode - end

Hibernate utilise 3 requêtes pour effectuer le chargement. C'est un problème important. Imaginez que votre base contient 1000 items qui eux-mêmes référencent 10 Bids différents... Nous n'optimisons pas vraiment notre chargement, cela nous coûte cher en nombre de requêtes SQL. De plus, n'étant pas en mode de chargement tardif, nous voyons bien qu'Hibernate effectue les 3 requêtes avant d'attaquer l'affichage de notre résultat.

Que se passe-t-il lorsque le mode lazy est actif ?
Rappelons que par défaut Hibernate étant bien fait, ce mode est implicite et il est le mode de fonctionnement par défaut d'Hibernate. Pour ce test, je me contente de changer dans le fichier Item.hbm.xml la configuration du chargement tardif et je relance le même test afin de voir comment Hibernate charge la collection:

<hibernate-mapping>

    <class name="org.touilleur.hibernate.v1.Item" table="ITEM">
        <id name="id" column="ITEM_ID">
            <generator class="native"/>
        </id>
        <property name="name"/>
        <!-- The fetch join strategy uses a select join query to load with one transact the set of bids -->
        <property name="price"/>
 <-- LAZY est à TRUE -->
        <set name="bids" table="ITEM_BIDS" lazy="true" inverse="true" fetch="select" cascade="all">
            <key column="ITEM_ID" not-null="true"/>
            <one-to-many class="org.touilleur.hibernate.v1.Bid"/>
        </set>
    </class>

</hibernate-mapping>

Je relance le même test unitaire :

INFO 2009-03-03 10:26:38,910 [Demo Hibernate] : testSelecTAllItemsWithDefaultFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_0_, this_.name as name0_0_, this_.price as price0_0_ from ITEM this_
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
INFO 2009-03-03 10:26:38,937 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=1, amount=101.0, date=2009-03-03 10:26:38.864}, Bid{id=10, amount=110.0, date=2009-03-03 10:26:38.883}, Bid{id=8, amount=108.0, date=2009-03-03 10:26:38.878}, Bid{id=3, amount=103.0, date=2009-03-03 10:26:38.867}, Bid{id=6, amount=106.0, date=2009-03-03 10:26:38.871}, Bid{id=2, amount=102.0, date=2009-03-03 10:26:38.866}, Bid{id=9, amount=109.0, date=2009-03-03 10:26:38.879}, Bid{id=5, amount=105.0, date=2009-03-03 10:26:38.87}, Bid{id=7, amount=107.0, date=2009-03-03 10:26:38.876}, Bid{id=4, amount=104.0, date=2009-03-03 10:26:38.868}], name='TV LCD', price=340.0}
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
INFO 2009-03-03 10:26:38,944 [Demo Hibernate] : Item #2 : Item{id=2, bids=[Bid{id=18, amount=138.0, date=2009-03-03 10:26:38.896}, Bid{id=11, amount=10.0, date=2009-03-03 10:26:38.885}, Bid{id=20, amount=144.0, date=2009-03-03 10:26:38.898}, Bid{id=16, amount=116.0, date=2009-03-03 10:26:38.893}, Bid{id=14, amount=84.0, date=2009-03-03 10:26:38.888}, Bid{id=19, amount=139.0, date=2009-03-03 10:26:38.897}, Bid{id=15, amount=125.0, date=2009-03-03 10:26:38.892}, Bid{id=12, amount=22.0, date=2009-03-03 10:26:38.886}, Bid{id=13, amount=43.0, date=2009-03-03 10:26:38.887}, Bid{id=17, amount=137.0, date=2009-03-03 10:26:38.895}], name='Plasma', price=1230.0}
INFO 2009-03-03 10:26:38,947 [Demo Hibernate] : testSelecTAllItemsWithDefaultFetchMode - end

Cette fois-ci nous constatons qu’Hibernate effectue une requête que lorsque cela devient nécessaire, ce qui est adapté dans la majorité des cas.
Lorsque le mode lazy est désactivé, Hibernate effectue n+1 requêtes sans jointure. Cela peut entraîner un très grand nombre de requêtes vers la base. Nous allons donc voir comment optimiser dans ce cas précis le chargement des associations.

Fetch strategy à join et lazy=false

Si nous ne pouvons pas utiliser le chargement tardif, il est alors intéressant d’utiliser une requête et d’effectuer une jointure entre la table Item et la table Bid. Pour cela il suffit de changer de stratégie lors de la requête. Nous passons maintenant le mode de récupération (FetchMode) à JOIN :

// voir la class org.touilleur.hibernate.SelectTest
 Item item1 = (Item) session.createCriteria(Item.class)
                .setFetchMode("bids", FetchMode.JOIN)
                .add(Restrictions.idEq(id))
                .uniqueResult();

Si vous savez qu’il sera toujours plus intéresant d’utiliser une requête par jointure vous pouvez aussi changer la stratégie dans le fichier Item.hbm.xml :

<set name="bids" table="ITEM_BIDS" lazy="false" inverse="true" fetch="join" cascade="all">

Le test selectOneItemWithJoin nous montre alors qu’une seule requête est exécutée, et qu’une jointure externe gauche entre la table Item et la table Bid permet de retrouver un Item avec 0 ou plusieurs Bid.


INFO 2009-02-27 22:24:21,772 [Demo Hibernate] : Starting selectOneItemWithJoin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_1_, this_.name as name0_1_, this_.price as price0_1_,
bids2_.ITEM_ID as ITEM4_3_, bids2_.BID_ID as BID1_3_, bids2_.BID_ID as BID1_1_0_, bids2_.amount as amount1_0_,
bids2_.date as date1_0_, bids2_.ITEM_ID as ITEM4_1_0_
from ITEM this_ left outer join BID bids2_
on this_.ITEM_ID=bids2_.ITEM_ID where this_.ITEM_ID = ?
INFO 2009-02-27 22:24:21,775 [Demo Hibernate] : Loaded item1 with a FetchMode set to JOIN

Notez qu’Hibernate utilise une jointure externe de la table Item vers la table Bid, cela nous donne alors en simplifiant le code SQL la requête suivante :
select i.*,b* from ITEM i
outer join BID b
on i.ITEM_ID=b.ITEM_ID

Par défaut il n’y a donc pas de critères de distinction, et nous nous retrouvons alors avec 20 résultats, ce qui sera pratique pour remplir un tableau mais qui n’est pas forcément souhaitable pour votre code.

INFO 2009-03-03 10:30:53,041 [Demo Hibernate] : testSelecTAllItemsWithJoinFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_1_, this_.name as name0_1_, this_.price as price0_1_, bids2_.ITEM_ID as ITEM4_3_, bids2_.BID_ID as BID1_3_, bids2_.BID_ID as BID1_1_0_, bids2_.amount as amount1_0_, bids2_.date as date1_0_, bids2_.ITEM_ID as ITEM4_1_0_ from ITEM this_ left outer join BID bids2_ on this_.ITEM_ID=bids2_.ITEM_ID
INFO 2009-03-03 10:30:53,072 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=3, amount=103.0, date=2009-03-03 10:30:52.999}, Bid{id=8, amount=108.0, date=2009-03-03 10:30:53.01}, Bid{id=6, amount=106.0, date=2009-03-03 10:30:53.003}, Bid{id=10, amount=110.0, date=2009-03-03 10:30:53.014}, Bid{id=5, amount=105.0, date=2009-03-03 10:30:53.002}, Bid{id=2, amount=102.0, date=2009-03-03 10:30:52.998}, Bid{id=7, amount=107.0, date=2009-03-03 10:30:53.009}, Bid{id=4, amount=104.0, date=2009-03-03 10:30:53.001}, Bid{id=9, amount=109.0, date=2009-03-03 10:30:53.011}, Bid{id=1, amount=101.0, date=2009-03-03 10:30:52.997}], name='TV LCD', price=340.0}
INFO 2009-03-03 10:30:53,074 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=3, amount=103.0, date=2009-03-03 10:30:52.999}, Bid{id=8, amount=108.0, date=2009-03-03 10:30:53.01}, Bid{id=6, amount=106.0, date=2009-03-03 10:30:53.003}, Bid{id=10, amount=110.0, date=2009-03-03 10:30:53.014}, Bid{id=5, amount=105.0, date=2009-03-03 10:30:53.002}, Bid{id=2, amount=102.0, date=2009-03-03 10:30:52.998}, Bid{id=7, amount=107.0, date=2009-03-03 10:30:53.009}, Bid{id=4, amount=104.0, date=2009-03-03 10:30:53.001}, Bid{id=9, amount=109.0, date=2009-03-03 10:30:53.011}, Bid{id=1, amount=101.0, date=2009-03-03 10:30:52.997}], name='TV LCD', price=340.0}
...
Message répété 20 fois
...
INFO 2009-03-03 10:30:53,125 [Demo Hibernate] : testSelecTAllItemsWithJoinFetchMode - end

Pour ajouter une clause DISTINCT, nous pouvons modifier notre critère et utiliser un ResultTransformer :

List l = session.createCriteria(Item.class)
                .setFetchMode("bids", FetchMode.JOIN)
                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                .list();

En exécutant à nouveau notre test voici le résultat :

INFO 2009-03-03 10:43:31,196 [Demo Hibernate] : testSelecTAllItemsWithJoinFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_1_, this_.name as name0_1_, this_.price as price0_1_, bids2_.ITEM_ID as ITEM4_3_, bids2_.BID_ID as BID1_3_, bids2_.BID_ID as BID1_1_0_, bids2_.amount as amount1_0_, bids2_.date as date1_0_, bids2_.ITEM_ID as ITEM4_1_0_ from ITEM this_ left outer join BID bids2_ on this_.ITEM_ID=bids2_.ITEM_ID
INFO 2009-03-03 10:43:31,229 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=8, amount=108.0, date=2009-03-03 10:43:31.162}, Bid{id=7, amount=107.0, date=2009-03-03 10:43:31.16}, Bid{id=2, amount=102.0, date=2009-03-03 10:43:31.149}, Bid{id=6, amount=106.0, date=2009-03-03 10:43:31.154}, Bid{id=10, amount=110.0, date=2009-03-03 10:43:31.167}, Bid{id=9, amount=109.0, date=2009-03-03 10:43:31.163}, Bid{id=3, amount=103.0, date=2009-03-03 10:43:31.15}, Bid{id=5, amount=105.0, date=2009-03-03 10:43:31.153}, Bid{id=1, amount=101.0, date=2009-03-03 10:43:31.147}, Bid{id=4, amount=104.0, date=2009-03-03 10:43:31.151}], name='TV LCD', price=340.0}
INFO 2009-03-03 10:43:31,232 [Demo Hibernate] : Item #2 : Item{id=2, bids=[Bid{id=19, amount=139.0, date=2009-03-03 10:43:31.181}, Bid{id=17, amount=137.0, date=2009-03-03 10:43:31.179}, Bid{id=15, amount=125.0, date=2009-03-03 10:43:31.176}, Bid{id=13, amount=43.0, date=2009-03-03 10:43:31.171}, Bid{id=11, amount=10.0, date=2009-03-03 10:43:31.169}, Bid{id=12, amount=22.0, date=2009-03-03 10:43:31.17}, Bid{id=14, amount=84.0, date=2009-03-03 10:43:31.172}, Bid{id=20, amount=144.0, date=2009-03-03 10:43:31.182}, Bid{id=18, amount=138.0, date=2009-03-03 10:43:31.18}, Bid{id=16, amount=116.0, date=2009-03-03 10:43:31.177}], name='Plasma', price=1230.0}
INFO 2009-03-03 10:43:31,235 [Demo Hibernate] : testSelecTAllItemsWithJoinFetchMode - end

Voir la FAQ Hibernate [Hibernate does not return distinct results for a query with outer join fetching enabled for a collection (even if I use the distinct keyword)]

La sélection par jointure est donc une option possible. Nous sommes passés de 3 requêtes à une seule requête, ce qui est une optimisation permettant de réduire le nombre de requête SQL. Encore une fois, c’est à vous et à votre DBA de décider de la meilleur stratégie, il ne faut donc pas appliquer à l’aveugle les paramètres. Les jointures sont efficaces si vos colonnes de jointure sont correctement indexées, il est donc important de s’assurer que votre administrateur de base de données a aussi validé vos changements.

Fetch mode par subselect

Hibernate propose un mode de sélection moins connu et pourtant très efficace sur les associations, le mode subselect. L’idée est simplement d’utiliser le résultat d’une première requête comme critère de la sélection de la deuxième requête pour effectuer une requête plus efficace.

Ce que nous avons vu en premier, en mode lazy=false et fetch=select, c’est qu’Hibernate effectue n+1 requêtes pour charger la liste des Bids pour chacun des Items.

/* Retrouve la liste des Items */
select * from ITEM

/* Retrouve la list des Bids pour chaque Item */
select * from BID  where ITEM_ID = 12
select * from BID  where ITEM_ID = 34
select * from BID  where ITEM_ID = 37
select * from BID  where ITEM_ID = 39

Il y deux façons d’optimiser ce type de requête : soit utiliser le paramètre batch-size dans l’element set du fichier Item.hbm.xml, soit utiliser une requête de sous sélection. Je ne sais pas si ce mode est supporté par toutes les bases de données, mais voici le principe :
– Sélectionner les Items
– Sélectionner les Bids en utilisant les ID des items précedemment trouvé

/* Retrouve la liste des Items */
select * from ITEM

/* Retrouve la list des Bids pour chaque Item */
select * from BID  where ITEM_ID IN( 12, 34, 37, 39)

Pour cela il n’est pas possible de changer le mode de sélection dans le code, il faut éditer le fichier Item.hbm.xml et préciser que le fetch type est subselect

<hibernate-mapping>
  <class name="org.touilleur.hibernate.v1.Item" table="ITEM">
        <id name="id" column="ITEM_ID">
            <generator class="native"/>
        </id>
        <property name="name"/>
        <!-- Nous testons subselect -->
        <property name="price"/>
        <set name="bids" table="ITEM_BIDS" lazy="false"
               inverse="true"
               fetch="subselect"
               cascade="all">
            <key column="ITEM_ID" not-null="true"/>
            <one-to-many class="org.touilleur.hibernate.v1.Bid"/>
        </set>
    </class>

</hibernate-mapping>

Après avoir édité le fichier Item.hbm.xml, le test unitaire testSelecTAllItemsWithSubSelectFetchMode nous montre que deux requêtes sont exécutées au lieu de n+1 dans le cas du mode « select » ou une seule requête dans le cas du mode « join » :

INFO 2009-03-03 11:06:36,678 [Demo Hibernate] : testSelecTAllItemsWithSubSelectFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_0_, this_.name as name0_0_, this_.price as price0_0_ from ITEM this_
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */
select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_,
bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_,
bids0_.ITEM_ID as ITEM4_1_0_
from BID bids0_
where bids0_.ITEM_ID
in (select this_.ITEM_ID from ITEM this_)
INFO 2009-03-03 11:06:36,711 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=7, amount=107.0, date=2009-03-03 11:06:36.646}, Bid{id=3, amount=103.0, date=2009-03-03 11:06:36.636}, Bid{id=8, amount=108.0, date=2009-03-03 11:06:36.647}, Bid{id=2, amount=102.0, date=2009-03-03 11:06:36.635}, Bid{id=6, amount=106.0, date=2009-03-03 11:06:36.64}, Bid{id=5, amount=105.0, date=2009-03-03 11:06:36.639}, Bid{id=1, amount=101.0, date=2009-03-03 11:06:36.633}, Bid{id=9, amount=109.0, date=2009-03-03 11:06:36.648}, Bid{id=4, amount=104.0, date=2009-03-03 11:06:36.637}, Bid{id=10, amount=110.0, date=2009-03-03 11:06:36.652}], name='TV LCD', price=340.0}
INFO 2009-03-03 11:06:36,714 [Demo Hibernate] : Item #2 : Item{id=2, bids=[Bid{id=14, amount=84.0, date=2009-03-03 11:06:36.657}, Bid{id=18, amount=138.0, date=2009-03-03 11:06:36.664}, Bid{id=17, amount=137.0, date=2009-03-03 11:06:36.663}, Bid{id=20, amount=144.0, date=2009-03-03 11:06:36.666}, Bid{id=15, amount=125.0, date=2009-03-03 11:06:36.66}, Bid{id=12, amount=22.0, date=2009-03-03 11:06:36.655}, Bid{id=13, amount=43.0, date=2009-03-03 11:06:36.656}, Bid{id=19, amount=139.0, date=2009-03-03 11:06:36.665}, Bid{id=16, amount=116.0, date=2009-03-03 11:06:36.661}, Bid{id=11, amount=10.0, date=2009-03-03 11:06:36.654}], name='Plasma', price=1230.0}
INFO 2009-03-03 11:06:36,716 [Demo Hibernate] : testSelecTAllItemsWithSubSelectFetchMode - end

L’intérêt de ce mode de sélection est de récuperer en deux requêtes l’ensemble des Items et ensuite des Bids. Si vous faites enfin le test en mode « lazy=true » vous verrez par ailleurs qu’Hibernate n’effectue pas de chargement tardif mais qu’il charge toutes les données. Le mode de sélection en « SUBSELECT » limite à deux requêtes le nombre d’interrogation nécessaire, le tout sans jointure. C’est donc une option intéressante à tester selon la logique de votre application.

Fetch mode par batch size

La 4ème technique pour améliorer les chargements par lot est d’activer l’option batch-size. Afin de montrer son fonctionnement nous allons d’abord limiter la taille de chargement à 1 pour le nombre d’Item et le nombre de collection de Bid :

<hibernate-mapping>

    <class name="org.touilleur.hibernate.v1.Item" table="ITEM" batch-size="1">
        <id name="id" column="ITEM_ID">
            <generator class="native"/>
        </id>
        <property name="name"/>
        <!-- The fetch join strategy uses a select join query to load with one transact the set of bids -->
        <property name="price"/>
        <set name="bids" table="ITEM_BIDS"
              lazy="false"
              inverse="true"
              fetch="select"
              cascade="all"
              batch-size="1">
            <key column="ITEM_ID" not-null="true"/>
            <one-to-many class="org.touilleur.hibernate.v1.Bid"/>
        </set>
    </class>

</hibernate-mapping>

Nous voyons qu’à l’exécution, Hibernate effectue une requete pour récuperer une liste de deux Items puis ensuite deux requêtes pour chacun des Items. En mode lazy, nous aurions simplement vu que la deuxième requête de chargement aurait été effectuée plus tardivement


INFO 2009-03-03 13:19:19,261 [Demo Hibernate] : testSelecTAllItemsWithSubSelectFetchMode - begin
Hibernate: /* criteria query */ select this_.ITEM_ID as ITEM1_0_0_, this_.name as name0_0_, this_.price as price0_0_ from ITEM this_
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
Hibernate: /* load one-to-many org.touilleur.hibernate.v1.Item.bids */ select bids0_.ITEM_ID as ITEM4_1_, bids0_.BID_ID as BID1_1_, bids0_.BID_ID as BID1_1_0_, bids0_.amount as amount1_0_, bids0_.date as date1_0_, bids0_.ITEM_ID as ITEM4_1_0_ from BID bids0_ where bids0_.ITEM_ID=?
INFO 2009-03-03 13:19:19,294 [Demo Hibernate] : Item #1 : Item{id=1, bids=[Bid{id=2, amount=102.0, date=2009-03-03 13:19:19.217}, Bid{id=5, amount=105.0, date=2009-03-03 13:19:19.221}, Bid{id=3, amount=103.0, date=2009-03-03 13:19:19.218}, Bid{id=10, amount=110.0, date=2009-03-03 13:19:19.233}, Bid{id=7, amount=107.0, date=2009-03-03 13:19:19.227}, Bid{id=4, amount=104.0, date=2009-03-03 13:19:19.219}, Bid{id=1, amount=101.0, date=2009-03-03 13:19:19.215}, Bid{id=9, amount=109.0, date=2009-03-03 13:19:19.23}, Bid{id=8, amount=108.0, date=2009-03-03 13:19:19.229}, Bid{id=6, amount=106.0, date=2009-03-03 13:19:19.222}], name='TV LCD', price=340.0}
INFO 2009-03-03 13:19:19,297 [Demo Hibernate] : Item #2 : Item{id=2, bids=[Bid{id=19, amount=139.0, date=2009-03-03 13:19:19.247}, Bid{id=16, amount=116.0, date=2009-03-03 13:19:19.243}, Bid{id=11, amount=10.0, date=2009-03-03 13:19:19.236}, Bid{id=18, amount=138.0, date=2009-03-03 13:19:19.246}, Bid{id=13, amount=43.0, date=2009-03-03 13:19:19.237}, Bid{id=20, amount=144.0, date=2009-03-03 13:19:19.248}, Bid{id=14, amount=84.0, date=2009-03-03 13:19:19.238}, Bid{id=15, amount=125.0, date=2009-03-03 13:19:19.242}, Bid{id=17, amount=137.0, date=2009-03-03 13:19:19.245}, Bid{id=12, amount=22.0, date=2009-03-03 13:19:19.236}], name='Plasma', price=1230.0}
INFO 2009-03-03 13:19:19,300 [Demo Hibernate] : testSelecTAllItemsWithSubSelectFetchMode - end

Le chargement par lot est une technique efficace qui permet de précharger un nombre défini de proxy non initialisé si un premier proxy est accedé. Il y a donc l’optimisation au niveau du chargement de l’entité (Item) et l’optimisation au niveau du chargement de la collection (Bid).

Le chargement par lot pour notre classe Item fonctionne de la manière suivante : dans votre base imaginons que nous ayons 10 Items, chacun de ces Items référencent un Acheteur avec une relation un-à-un. La relation est mappée avec un proxy en mode lazy= »true ». Si par défaut vous chargez la liste des Items puis qu’ensuite vous appelez la méthode getBuyer() pour retrouver l’acheteur, Hibernate effectuera alors 10 requêtes SQL (10 items).
Le chargement par lot permet de spécifier à Hibernate la taille d’une fenêtre de chargement, en utilisant les clés primaires ou les clés étrangères, qui ici seraient dans la table BUYER.


<class name="Item" batch-size="10">...</class>

Hibernate exécutera non plus 10 requêtes SQL mais une seule pour charger l’ensemble des acheteurs. Idéalement vous l’aurez deviné, ce batch-size correspond au nombre d’éléments affichés sur une page Web, dans un tableau paginé par exemple.

Ensuite il est possible d’activer le chargement par lot sur les collections. Notre Item a une collection de Bid. Imaginons que je charge mes 10 Items dans ma Session Hibernate. Par défaut, chaque appel à getBids() entrainera alors une requête SQL, soit 10 requêtes pour charger chacune des collections. En spécifiant une taille de batch comme ci-dessous il est possible d’optimiser le nombre d’éléments préchargés et donc de réduire le nombre de requête SQL :

<class name="Item">
    <set name="Bid" batch-size="3">
        ...
    </set>
</class>

Pour 7 enchères, Hibernate chargera alors 3,3,1 en effectuant 3 requêtes au lieu de 7 requêtes par exemple. Cela permet de réduire encore une fois le nombre de requête, et cette valeur doit être guidée par le code de votre application et l’usage que vous en faîtes.
Pour cette raison il est souvent plus souhaitable de définir la taille du batch au niveau du code comme dans ci-dessous:

 List l = session.createCriteria(Item.class)
                    .setFetchSize(10)
                    .list();

Petite astuce au passage : si vous souhaitez limiter le nombre de résultat retourné, vous connaissez sans doute la commande setMaxResults(int i).

List l = session.createCriteria(Item.class)
                    .setMaxResults(10)
                    .list();

Par curiosité je me suis un peu demandé pourquoi Hibernate prend plus de temps que la même requête SQL exécutée sur ma base de test. En fouillant un peu je me suis aperçu qu’il est intéressant de préciser la taille du batch afin de ne récuperer qu’un seul lot :

List l = session.createCriteria(Item.class)
                    .setMaxResults(10)
                    .setFetchSize(10)
                    .list();

Essayez sur votre code vous verrez la différence.

Conclusion de la première partie

Lorsque le chargement tardif ne peut pas être utilisé, il est important de vérifier que la stratégie par défaut d’Hibernate n’entraine pas un nombre de requêtes SQL trop importantes. Il est possible de limiter ce nombre en utilisant des jointures ou des selections imbriquées. A vous ensuite d’optimiser vos réglages selon votre application.

Dans la deuxième partie nous allons voir les 3 paramètres différents de chargement des collections.

J’espère que vous aurez apprécié. Si vous voulez aller un peu plus loin, il ne vous reste plus qu’à télécharger le code de l’article et à commencer à tester les différentes configurations possibles d’Hibernate. Pour les personnes à la recherche d’un tutorial sur Hibernate, la première partie vous montre un exemple simple de mapping pour commencer à apprendre Hibernate.

Bonne lecture et à bientôt pour la deuxième partie !

Code source de l’article
ArticleTouilleur_hibernate.tar.gz

4 réflexions sur « Hibernate : gérer le chargement des associations efficacement »

  1. Excellant article !! Très complet à en croire la première partie, j’ai hâte de lire la seconde !

    Comme tu l’expliques: Mieux vaut éviter un maximum de mettre dans le mapping du paramétrage en relation avec les IHMs. On privilégiera la configuration spécifique du batch size par le code.

    Pas mal l’astuce sur la fin, toujours bon à savoir: criteria.setMaxResults(10).setFetchSize(10);

    Et pourquoi dans Hibernate les attributs ne correspondent jamais à la méthode java équivalente… pour le coup en XML c’est batch-size et setFetchSize en Java…

    Et pour conclure, avec les annotations c’est @BatchSize(size=4)

    Tu as la pression pour la seconde partie !!!

    A +

  2. Cyrille: … et je n’ai même pas fait la version avec annotations…
    La seconde partie sera plus courte

  3. Très bon article :). Je suis toujours étonné de la qualité de tes posts.

    J\’ai récemment effectué des travaux pour l\’audit et l\’optimisation d\’utilisation d\’Hibernate, et ton article va droit à l\’essentiel par des explications simples.

    En XML il me semble que le fetch-size indique la profondeur maximum lors du chargement des associations non tardivement. Il s\’agit d\’une sécurité qu\’il aurait peut être fallut préciser.

    Au passage, ton application exemple ne permet pas de lancer les tests depuis la commande maven \ »mvn test\ » : il faut replacer les sources du dossier test avec le chemin \ »src\\test\\java\ » et non \ »src\\main\\test\ »

    J\’attend impatiemment le second article

  4. J’ai corrigé l’archive tar.gz et déplacé les tests unitaires dans src\test\java

    Le deuxième article est en ligne sinon !

Les commentaires sont fermés.