Dites adieu aux LazyInitializationException

Dans ce deuxième ticket, j’aimerais vous parler du projet Hibernate Hydrate [1] que j’ai récemment publié sur GitHub. Au cœur de ce projet : une seule classe Java proposant une unique fonctionnalité. En quelques années, c’est la seconde fois que j’ai eu besoin de coder ce genre de fonctionnalité. Aussi, je me suis dit qu’il serait pratique de l’avoir sous le coude pour une prochaine fois et, au passage, vous en faire profiter.
logo_hibernate

Origine des lazy exceptions

En quoi consistent ce projet et cette fameuse fonctionnalité ? Eh bien, sous certaines conditions, résoudre un problème récurrent lors de l’utilisation d’Hibernate. En effet, lorsque l’on tente d’accéder à un objet détaché de la session Hibernate, ce dernier n’est pas forcément entièrement chargé en mémoire : son proxy ou ses propriétés peuvent ne pas être initialisés, ce qui est par exemple le cas d’une relation déclarée comme paresseuse (ou lazy). Et c’est à cet instant-là, qu’Hibernate lève la tant redoutée LazyInitializationException.
Par objet détaché, j’entends un objet évincé de la session (retirée du cache de premier niveau par un session.clear() ou un evict()) ou dont la session est fermée (session.close())
Dans une application, ce phénomène est susceptible de se produire à plusieurs niveaux :

  • Couche présentation : contrôleur (ex : action Struts) ou rendu de la vue (ex : JSP)
  • Exposition d’un web service : marshalling XML, mapping dozer …

La documentation d’Hibernate propose plusieurs solutions pour remédier à ce problème. Plus encore, elle explique ce qu’il faut éviter de faire, comme par exemple ouvrir une autre unité de travail (transaction) pour charger les données manquantes.
Solutions préconisées

Pattern Open Session In View

Une première solution consiste à utiliser le pattern Open Session In View [2]. Dans une application web, ce pattern peut par exemple être implémenté à l’aide d’un filtre de servlets. L’arrivée d’une requête HTTP initie l’ouverture d’une transaction et de la session Hibernate correspondante. Une fois la vue rendue et prête être renvoyée au client, la session est fermée et la transaction est validée.
Le pattern Open Session In View ne peut pas s’appliquer dans une architecture technique 3 tiers où la couche présentation et la couche métier sont déployées physiquement sur 2 serveurs différents, et donc 2 JVMs. Ce pattern n’est également pas valable dans le cadre d’une application web riche utilisant Ajax et JavaScript pour récupérer puis parcourir le modèle métier.

Pré-chargement sur mesure

Personnellement, je recommande généralement à ce que les transactions soient gérées au niveau de la couche métier. En effet, à ce niveau, le service métier connait l’usage des objets qu’il va charger depuis la base de données. Il est capable d’utiliser le DAO ayant la stratégie de pré-chargement [3] (ou fetching) adaptée au traitement métier.

Pour rappel, le pré-chargement des relations peut être configuré de 2 manières :

  1. statiquement au niveau du mapping Hibernate (en XML ou par annotations)
  2. dynamiquement lors du requêtage en HQL (JOIN FETCH) ou par l’API Criteria (méthode setFetchMode())

Par défaut, dans Hibernate 3.x, les associations vers une autre entité ou une collection d’entités sont chargées tardivement ; c’est-à-dire à la demande, lorsque l’on essaie d’accéder à l’entité ou à la collection (et que la session est ouverte). Qui plus est, les stratégies définies statiquement ne sont pas forcément utilisées lors d’un requêtage HQL.

Une bonne pratique issue du guide de référence d’Hibernate [4] consiste à conserver le comportement par défaut d’Hibernate et à redéfinir la stratégie de pré-chargement à chaque usage. Cela permet d’optimiser votre code et de ramener les données dont vous avez strictement besoin.

Comme illustré dans l’article Hibernate Survival Guide [5], et grâce au cache de premier niveau d’Hibernate, il est parfois plus performant de découper sa requête en plusieurs requêtes, notamment lorsque la grappe d’objets est complexe et la cardinalité des associations importante.


Hybernate Hydrate à la rescousse

La solution que je vais vous présenter peut être utilisée conjointement avec la solution du pré-chargement sur mesure.

La méthode statique deepHydrate de la classe LazyLoadingUtil permet de charger dans sa globalité la grappe d’objets qui lui est passée en paramètre. Seule contrainte, cette méthode doit être appelée avant que la session Hibernate et la transaction associée ne soient clôturées.

Voici un exemple d’utilisation :

Techniquement, la méthode deepHydrate() utilise le méta-modèle Hibernate (interface ClassMetadata) pour parcourir l’ensemble du graphe des objets persistés et déterminer le type des propriétés et des relations du modèle. Ainsi, elle peut naviguer récursivement dans le graphe. Les proxy rencontrés sont initialisés puis résolus. Les collections persistantes sont initialisés puis itérés.

Dans le cas d’un graphe cyclique, un mécanisme de garde permet d’éviter toute boucle infinie.

La classe TestLazyLoadingUtil propose des exemples d’utilisation.

Une variante est disponible pour les applications utilisant JPA avec Hibernate pour provider : JpaLazyLoadingUtil.

Pour l’essayer, vous avez le choix entre un copier / coller, un git clone ou bien l’ajout d’une dépendance maven et du repo qui va avec :

Les artefacts du projet Hybernate Hydrate sont disponibles sur Maven Central.

Conclusion

Pour terminer cette présentation, voici un tableau récapitulatif qui devrait vous permettre d’orienter votre choix sur l’usage ou non de cette petite librairie :

Scénarios pour lesquels écarter cette solution Scénarios favorables à son utilisation
  1. Utilisation du pattern Open Session in View
  2. Problématiques fortes de performance
  3. Grappes d’objets risquant de remonter toute la base de données
  1. Grappe d’objets constituée de nombreuses classes
  2. Développement rapide d’une application avec chantier d’optimisations portant sur les requêtes critiques
  3. Exploitation du cache de niveau 2 d’Hibernate pour les données en « bout de grappe » (ex : données du référentiel).
  4. Outil ayant besoin de charger toute une grappe d’objets en mémoire

De mon côté, ce modeste travail de capitalisation m’a permis de renouer avec l’open-source ;  en tant que contributeur j’entends. Mon dernier code poussé sur Source Forge datait en effet de l’an 2000 …
Cela m’aura également permis de me familiariser davantage avec GitHub : syntaxe MarkDown, pages wiki gérées par git ou bien encore les releases avec maven. Ce dernier point fera d’ailleurs l’objet d’un prochain article.
J’ai également pu adhérer au programme Free and OpenSource (FOSS) de CloudBees, ce qui vous permet d’accéder librement au Jenkins d’Hibernate Hydrate [6].

Enfin, si vous êtes un jour amené à utiliser ce code, je serais intéressé de le savoir. Et si vous voulez contribuer (ex : support d’autres ORM, annotation + post-processeur Spring …), les portes sont grandes ouvertes.

Références :

  1. Projet Hibernate Hydrate hébergé sur Github
  2. Open Session In View du wiki Hibernate de JBoss
  3. A Short Primer On Fetching Strategies du wiki Hibernate de JBoss
  4. Performance fetching du guide de référence d’Hibernate
  5. Hibernate Survival Guide du wiki d’Object Direct
  6. Jenkins d’Hibernate Hydrate hébergé sur la plateforme DEV@Cloud de CloudBees[6].

4 réflexions au sujet de « Dites adieu aux LazyInitializationException »

  1. Heuuuu…. Franchement c’est un peu n’imp ce projet. Pour un projet bien configurer, je veux dire ayant les liens objets qu’il faut ou il faut, il est très souvent possible de ramener L’ENSEMBLE des données à partir d’un seul objet si on parcours assez profondément le graphe.

    Donc à moins qu’on veuille faire péter la mémoire de la JVM, et les perfs par la même occaz ! , je déconseille vivement !!!

    (incroyable que ce soit en prermière page de google c’te page… comme quoi leurs algos sont pas encore tout à fait au point !)

    • Bonjour Philippe,
      Avant de critiquer ce projet, as-tu pris le temps de lire l’article en entier et les mises en garde qui vont avec ? En effet, Hybernate Hydrate n’est pas à utiliser sans réfléchir à la moindre LazyInitializationException.
      Lors de la définition du mapping Hibernate, une bonne pratique consiste à laisser la stratégie de chargement par défaut (ex: lazy pour du OneToMany). Les Entity Graphs permettent de déclarer différents scénarios de chargement des relations. C’est au requêtage que le développeur doit se demander quelles sont les données dont il va avoir besoin, et ne pas tout charger dans la majorité des cas. Nul besoin d’Hibernate Hydrate dans ce cas là. On est tous les deux d’accord.
      Néanmoins, comme expliqué dans ce billet, cet outil est fort pratique dans des conditions bien particulières. Personnellement, je l’utilise par exemple dans un outil d’échantillonnage de données nécessitant de charger toute une grappe d’objets (amuse toi à charger manuellement 50 entités). Les performances (nombre de requêtes SQL exécutés) n’est pas un soucis dans mon cas.
      J’espère que la 2ième page de résultats de Goole aura répondu à ta problématique,
      Antoine

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *