Hibernate/Lazy loading

Un article de JTips.
Hibernate / Lazy loading
Auteur : Olivier Hanny
Formation(s) sur le sujet :
Développement Hibernate
Icodem.jpg

Objectif du lazy loading

Pour comprendre pourquoi Hibernate utilise le lazy loading, il faut commencer par étudier le mécanisme du chargement immédiat. A cette fin, prenons l'exemple du diagramme de classe ci-dessous :

Hiblazy1.png

Remarquez que les associations sont toutes bi-directionnelles (approche préconisée par Hibernate, notamment afin de simplifier le requêtage HQL).

Quand on parle de chargement immédiat, il faut raisonner au niveau d'une association, pour un sens de navigation. Dans notre exemple, nous supposons donc que toutes les associations sont paramétrées en mode de chargement immédiat (lazy=false) :

<class name="Produit" table="PRODUIT">
  ...
  <many-to-one name="categorie" 
              column="CATEGORIE_ID"
              class="Categorie" 
              lazy="false"/>
  <set name="auteurs" table="PRODUIT_AUTEUR" lazy="false">
    <key column="PRODUIT_ID"/>
    <many-to-many class="Auteur" column="AUTEUR_ID"/>
  </set>
</class>

<class name="Categorie" table="CATEGORIE">
  ...
  <set name="produits" lazy="false">
    <key column="CATEGORIE_ID"/>
    <one-to-many class="Produit"/>
  </set>
</class>

<class name="Auteur" table="AUTEUR">
  ...
  <set name="produits" table="PRODUIT_AUTEUR" lazy="false">
    <key column="AUTEUR_ID"/>
    <many-to-many class="Produit" column="PRODUIT_ID"/>
  </set>
</class>

Regardons maintenant ce qui se passe lorsqu'on effectue la lecture d'un enregistrement, un produit par exemple :

Hiblazy2.png

Puisque l'on cherche à lire un produit à partir de son identifiant, Hibernate génère la requête SQL suivante :

select * from produit where id = ?;

Et comme on est en chargement immédiat de Produit vers Categorie, Hibernate génère aussi un SELECT sur la table CATEGORIE :

select * from categorie where id = ?;

Mais l'association Categorie -> Produit est aussi en chargement immédiat, et donc Hibernate génère en plus la requête suivante, puisque la catégorie associée au produit a été chargée :

select * from produit where categorie_id = ?;

Etendons maintenant ce raisonnement à l'association many-to-many Produit <-> Auteur. Pour chaque produit chargé (*), Hibernate effectue un SELECT vers la table des auteurs :

(*) n'oubliez pas qu'il n'y a plus qu'un seul produit chargé, mais plusieurs du fait du chargement immédiat de Categorie vers Produit.

select produit_auteur.*,auteur.* from produit_auteur left outer join auteur on produit_auteur.auteur_id=auteur.id where produit_auteur.produit_id=?

Et pour chaque auteur chargé, Hibernate génère un SELECT vers les produits associés :

select produit_auteur.*,produit.* from produit_auteur left outer join produit on produit_auteur.produit_id=produit.id where produit_auteur.auteur_id=?

Et si parmi les produits associés aux auteurs, certains n'étaient pas encore chargés, Hibernate relance à nouveau des requêtes vers la table CATEGORIE, puis AUTEUR pour chacun de ces produits...


Vous l'avez compris : le chargement immédiat est une très mauvaise stratégie car elle implique l'exécution de nombreuses requêtes SQL et l'instanciation d'un graphe d'objets conséquent, alors que nous souhaitions lire le contenu d'un seul produit (pas les objets associés) ! Cette solution entraîne évidemment des dégradations importantes des performances de l'application.

L'objectif du lazy loading est de pallier ce problème, en minimisant le nombre de requêtes générées en fonction des besoins applicatifs, tout en tenant compte des associations.

Principe du lazy loading

Le lazy loading concerne le chargement des associations, et dans certains cas le chargement des entités. Dans les paragraphes suivants, nous allons détailler le principe du lazy loading dans le cas des associations many-to-one, one-to-many. Nous examinerons aussi comment fonctionne la méthode Session.load() vis-à-vis du lazy loading.

Nous supposons dorénavant que toutes les associations et toutes les classes sont paramétrées en chargement lazy (lazy=true ou lazy=proxy), ce qui correspond en fait au paramétrage par défaut avec Hibernate 3 (le mode par défaut avec Hibernate 2 est le chargement immédiat).

Voici les fichiers de mapping correspondant à notre exemple :

<class name="Produit" table="PRODUIT" lazy="true">
  ...
  <many-to-one name="categorie" 
              column="CATEGORIE_ID"
              class="Categorie" 
              lazy="proxy"/>
  <set name="auteurs" table="PRODUIT_AUTEUR" lazy="true">
    <key column="PRODUIT_ID"/>
    <many-to-many class="Auteur" column="AUTEUR_ID"/>
  </set>
</class>

<class name="Categorie" table="CATEGORIE" lazy="true">
  ...
  <set name="produits" lazy="true">
    <key column="CATEGORIE_ID"/>
    <one-to-many class="Produit"/>
  </set>
</class>

<class name="Auteur" table="AUTEUR" lazy="true">
  ...
  <set name="produits" table="PRODUIT_AUTEUR" lazy="true">
    <key column="AUTEUR_ID"/>
    <many-to-many class="Produit" column="PRODUIT_ID"/>
  </set>
</class>


Les associations many-to-one

En mode lazy, suite à la lecture d'une entité, Hibernate ne charge pas les associations, i.e. Hibernate ne génère pas de requête SQL correspondant à ces associations. En revanche les instances des objets associés existent quand les foreign key ne sont pas nulles : en effet, si les champs d'instance correspondant aux associations étaient null, cela ne reflèterait pas la réalité des données en base.

Exemple : un produit est rattaché à une catégorie, et donc, lors du SELECT sur le produit recherché, la foreign key CATEGORIE_ID est récupérée (voir le SELECT ci-dessous) et doit être stockée dans un objet côté Java, afin de ne pas perdre cette information => le champ categorie dans l'instance de Produit ne doit pas être null.

select id, code, description, categorie_id from produit where id = ?

En fait, l'instance de catégorie est renseignée incomplètement : seul son identifiant est renseigné (avec la foreign key), les autres champs sont pour l'instant null. La figure ci-dessous illustre ce fonctionnement :

Hiblazy3.png

La problématique est alors la suivante : quand l'application accède au contenu de la catégorie (par exemple produit.getCategorie().getCode()), il ne faut pas retourner une valeur nulle (données incohérentes).

La technique utilisée par Hibernate consiste alors à générer un proxy : l'objet categorie instancié par Hibernate n'est pas strictement du type Categorie, il s'agit d'une sous-classe de Categorie (dans la figure ci-dessus, j'ai nommé cette sous classe Categorie$Enhanced.class : ce nom n'est pas exact, mais ce n'est pas très important, car dans notre code, nous ne devons jamais faire apparaître explicitement ces types). Intérêt du proxy : il permet de détecter le premier accès au contenu de l'objet catégorie et de générer un SELECT sur la table CATEGORIE à ce moment là :

select * from categorie where id = ?

Suite au SELECT, tous les champs de la catégorie sont renseignés, avant le retour de la méthode categorie.getCode(). Cette méthode retourne alors la valeur du code de la catégorie telle qu'elle est en base de données. Evidemment, si un autre accès au contenu de la catégorie est effectué (categorie.getXXX()), Hibernate ne génère pas à nouveau la requête sur la table CATEGORIE, puisque l'état de l'objet est déjà renseigné. La figure ci-dessous illustre ce fonctionnement :

Hiblazy4.png

Les associations one-to-many

Le principe du lazy loading est similaire à ce que nous venons de voir avec les associations many-to-one. La différence, c'est que dans ce cas Hibernate n'utilise pas de proxy. Cela est tout à fait normal : dans le code Java, une association many est représentée par une collection. Cet objet est défini par une des interfaces génériques de Java (Collection, List, Set, Map), et Hibernate peut donc fournir sa propre implémentation(*) afin de mettre en oeuvre le code d'interception, lors du premier accès au contenu de l'association (exemple : dans le cas d'un Set, Hibernate instancie un PersistentSet).

(*) c'est notamment pour cette raison que les côtés many doivent être déclarés avec les interfaces des collections, et non pas des types concrets (et de toute façon, c'est une bonne pratique !).

Hiblazy5.png

Lors du premier accès au contenu de la collection (exemple : categorie.getProduits().iterator()), Hibernate déclenche un SELECT vers la table correspondant aux objets associés (dans notre cas, la table PRODUIT) :

select * from produit where categorie_id = ?

Au retour du SELECT, Hibernate possède donc toutes les informations nécessaires pour instancier et renseigner complètement l'état des objets Produit associés à la catégorie (les produits ne sont pas des proxys). Ces objets sont en outre ajoutés dans la collection Categorie.produits.

Hiblazy6.png

La méthode Session.load()

Le lazy loading est aussi utilisé lorsque l'on effectue la lecture d'une entité à partir de son identifiant, via la méthode Session.load(). L'objet recherché avec Session.load() est instancié, mais la requête SQL correspondante n'est pas générée tout de suite. Donc comme dans le cas des associations many-to-one, Hibernate renseigne incomplètement l'état de l'objet (son identifiant essentiellement) => un proxy est utilisé.

Hiblazy7.png

Et comme précédemment, lors du premier accès au contenu de l'objet, Hibernate génère un SELECT et initialise complètement l'état de l'objet :

Hiblazy8.png

L'exception LazyInitializationException

La technique de rechargement retardée fonctionne lorsque la session Hibernate est ouverte. En effet, si l'on tente d'accéder à un proxy qui n'est pas encore initialisé, alors que la session est fermée (i.e. la connexion à la base de données est fermée), Hibernate ne peut pas générer de requête SQL : il en informe le code client par l'émission d'une LazyIntializationException.

Hiblazy9.png

La question que l'on se pose dans ce cas est alors la suivante : comment éviter ces exceptions ? On pourrait par exemple paramétrer nos associations en mode de chargement immédiat : évidemment, ce n'est pas la bonne réponse (cf premier paragraphe).

Une autre technique consiste à utiliser un design pattern nommé Open Session in View. Cette technique est applicable uniquement en environnement web. Il s'agit en fait d'installer un filtre de servlet qui ouvre la session Hibernate à la réception de la requête HTTP, et qui la ferme juste avant de renvoyer la réponse HTTP. Ainsi, quand les JSP tentent d'afficher le contenu des objets proxy non initialisés, Hibernate peut générer les requêtes SQL qui permettront d'initialiser ces proxys, puisque la session est encore ouverte. Mais ce pattern comporte un inconvénient majeur : le code de la couche métier n'est pas portable, puisqu'il doit être nécessairement exécuté en environnement web. En effet, si l'on souhaite utiliser notre code dans des composants distribués (RMI, Web Service...), on retrouvera les LazyInitializationException !

Voici une dernière approche : il s'agit d'identifier la profondeur d'initialisation des graphes d'objets nécessaire en retour de la couche de service métier, afin de répondre au besoin des couches clientes. Une fois que l'on sait exactement ce que l'on doit récupérer, il suffit d'initialiser ces graphes d'objets dans les services métiers avec la technique adaptée (lecture fetchée, navigation...). Avantage : on élimine les LazyInitializationException et le code de la couche métier est portable dans d'autres environnement que les applications web.


Quelques autres articles sur Hibernate