AXOPEN

Hibernate et la LazyInitializationException

Les développeurs utilisant Hibernate sont tôt ou tard amenés à se heurter à sa fameuse LazyInitializationException. Voyons dans quelles circonstances elle est levée et comment l'éviter définitivement.

Pourquoi la LazyInitializationException ?

Pour les victimes de la LazyInitializationException, les raisons de sa propagation sont souvent obscures et ils ont tendance à imaginer que son apparition est plus ou moins aléatoire. Il n'en est pourtant rien.

Contexte : le lazy loading

La LazyInitializationException n'apparaît que dans un contexte de lazy loading, litéralement "chargement paresseux". Concrètement, cela signifie que lorsque vous chargez une entité grâce à Hibernate, ses dépendances fonctionnelles en FetchType.LAZY ne seront jamais chargées tant que vous n'appellerez pas spécifiquement leur getter.

Exemple :

@Entity
@Table(name = "commande")
public class Commande implements Serializable {

        @ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "id_utilisateur")
	private Utilisateur utilisateur;

        public Utilisateur getUtilisateur() {
		return utilisateur;
	}

	public void setUtilisateur(Utilisateur pUtilisateur) {
		utilisateur = pUtilisateur;
	}
}

Ici, si je charge une commande avec Hibernate, j'ai bien mon objet Commande, mais je n'ai pas l'utilisateur. Ce n'est que lorsque j'appellerai la méthode getUtilisateur() qu'implicitement Hibernate passera la reqête SQL pour charger mon Utilisateur. 

Ce principe est valable pour le mapping en @ManyToOne aussi bien que pour le @OneToMany, le @OneToOne ou le @ManyToMany. Il est définit par la valeur de la propriété fetch, qui n'a que deux valeurs possibles : LAZY et EAGER (ce dernier signifie que les dépendances fonctionnelles sont chargées en même temps que l'objet principal : à utiliser avec parcimonie et subtilité si vous ne voulez pas récupérer l'ensemble de votre base de données à chaque requête). Le FetchType par défaut est EAGER pour @ManyToOne et @OneToOne, et LAZY pour @OneToMany et @ManyToMany.

Comment ça marche ?

Hibernate gère une grosse map de toutes les entités qu'il connaît. C'est ce qui lui permet de retrouver l'objet Utilisateur associé à mon objet Commande dans l'exemple précédent : puisqu'il connaît cette commande et qu'il connaît via le mapping le moyen de retrouver un Utilisateur associé à une Commande, il est en mesure de charger mon Utilisateur. 

Le problème de la LazyInitializationException vient de ce qu'Hibernate ne charge jamais de dépendances fonctionnelles d'objets qu'il ne connaît pas, c'est-à-dire qui ne sont pas dans sa map. Hors, la durée de vie d'une map Hibernate est celle de l'EntityManager utilisé. Autrement dit, la map est créée avec l'EntityManager et est perdue lorsque celui-ci est fermé. 

Exemple : dans le code ci-dessous, la map est créée à la ligne 6, en même temps que l'EntityManager, et est détruite à la fermeture de ce dernier ligne 17. La fin de la transaction ne détruit pas la map.

@PersistenceUnit(unitName = "test-manager")
private EntityManagerFactory emf;

public void maMethode() {
    try {
	em = emf.createEntityManager();
	em.getTransaction().begin();

	// votre code

	em.getTransaction().commit();
    } catch (Exception e) {
	e.printStackTrace();
    } finally {
	try {
            if (em != null)
		em.close();
            } catch (Throwable t) {
		e.printStackTrace();
	    }
    }
}

La levée de l'exception

Donc, toujours avec l'exemple de la commande, si j'invoque getUtilisateur() alors que (au choix) :

  • il n'y a pas d'EntityManager actif ; 
  • la map de l'EntityManager actif ne contient pas ma Commande ;

une LazyInitializationException est levée.

Comment y remédier ?

Très simplement, en rechargeant l'objet qui pose problème avec Hibernate. 

Exemple :

entityManager.find(Commande.class, commande.getId());

Ainsi l'EntityManager actif connaît de nouveau votre objet et vous pouvez profiter pleinement du LazyLoading.

Les situations les plus fréquentes

Le cas simple : changement de page

Mettons que j'ai un écran de listing de mes commandes : j'affiche leur numéro, leur montant, leur date, mais aucune information qui nécessite d'appeler getUtilisateur(). Dans ma Collection de Commandes, aucun n'objet n'aura son Utilisateur chargé. Si maintenant j'en sélectionne une via mon écran de gestion, et que je l'édite. Si dans l'édition j'affiche par exemple le nom de l'Utilisateur et que dans le code qui intervient dans le passage d'une page à l'autre je ne recharge pas ma Commande, j'obtiens une LazyInitializationException. Logique !

Le cas compliqué : la redirection 

Nous nous plaçons dans le cas de l'utilisation de l'API JSF (impl. Mojarra par exemple).

Reprenons le cas précédent : cette fois je recharge la commande sélectionnée, puis je redirige vers ma page d'édition de commande. J'obtiens encore une LazyInitializationException. Pourquoi ? Parce qu'il y a deux requêtes HTTP ! Le cheminement est le suivant :

  • mon formulaire est soumis en postback, c'est-à-dire avec comme URL d'action l'URL courante ;
  • dans mon code, je recharge mon objet puis je retourne un outcome de redirection ;
  • mon serveur d'application renvoie un code 302 pour indiquer une redirection ;
  • le navigateur client envoie alors une deuxième requête HTTP vers l'URL spécifiée dans la réponse 302 ;
  • alors seulement la page est construite et j'appelle getUtilisateur(). Mais bien sûr mon EntityManager n'a pas survécu entre les requêtes HTTP, il y en a donc un autre qui ne connaît pas ma Commande.

​Le fait qu'il y ait une redirection ou pas se paramétre dans les fichiers de navigation faces-config avec l'attribut redirect : s'il y est, il y a redirection, sinon non.

Comment remédier à cela ? On peut imaginer de multiples solutions, en voici une : utiliser un booléen placé dans le getter d'une entité pour signaler si elle doit être rechargée ou non (dans un Bean donc).

Exemple :

public Commande getCommande() {
    if(reload) {
        commande = getCommandeManager().getById(commande.getId);
        reload = false;
    }
    return commande;
}

Il n'y a plus qu'à gérer astucieusement le booléen (c'est-à-dire le passer à true pendant lors du premier appel pour que l'objet soit rechargé lors du second) et le tour est joué !