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 :
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 :
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é [1], Hibernate effectue un ----SELECT---- vers la table des auteurs :
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 :
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 :
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[2] 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----).
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----.
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é.
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 :
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.
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.