mardi 21 août 2007

Problème de suppression d'objets dans les collections avec Hibernate !

Hibernate est un framework de gestion de la persistance renommé et très connu qui permet bien des choses et avec lequel on peut s'arracher bien des cheveux !

Hibernate à la "facheuse" tendance à utiliser des types collection qui lui sont specifique pour gérer tout ce qui concerne les list, set... Les attributs de type list et set sont remplacés par des objets de type PersistentList et PersistentSet. Cela permet par exemple à hibernate de charger dynamiquement la collection lors de l'appelle à une méthode comme size() add() ou remove() dans le cas par exemple d'une collection mappée avec le lazy loading. Pour résumer, tant qu'on n'a pas besoin du contenu de la liste, elle est remplacé par un proxy qui est pret à al charger à la première demande.

Quand on utilise Hibernate dans le cadre d'une application ayant une architecture client-serveur, les classes d'hibernate ne sont accéssibles que coté serveur, coté base de données. Or dans une architecture client-serveur on fait transiter des objets métiers java de part et d'autre via le réseau (en HTTP par exemple avec Spring remoting) mais sous une condition : Que tous les objets envoyés soit accessibles dans les dépendances des 2 projets.

Or comme je l'ai expliqué juste avant, Hibernate remplace les listes par des objets qui lui sont propres et qui ne sont accéssibles qu'au projet serveur via la librairie hibernate (hibernate3.jar par exemple dans répertoire lib du serveur). Donc si on envoie au client une grappe métier contenant des listes après extraction de la base via hibernate, cela va poser problème et on va voir fleurir dans la console des erreurs du type "Exception : Ahhhhhh je ne connais pas la classe PersistentSet"

Il se peut qu'on ne soit pas impacté par ce problème car il est coutume d'utiliser des DTOs et des transformateurs pour faire transiter les objets sur le réseau et par conséquent les types spécifiques d'hibernate sont transformés automatiquement (par les transformateurs). Dans le cas ou nous n'avons pas de couche de DTOs, on peut utiliser des mappeurs qui remplacent les PersistentList et PersistentSet en ArrayList et HashpSet par exemple.

Bref, une fois les objets récupérés transformés (sans PersistentXXX) sur le client, on joue avec, ou les modifie et on peut être amené à supprimer des éléments d'une liste. Et c'est la que l'affaire se corse car il se peut, sans qu'on sache réellement pourquoi, que Hibernate ne puisse pas détecter la suppression d'un élément donné lors de la sauvegarde.

Effectivement, puisqu'on a remplacer les gestionnaire de liste à la sauce hibernate quand on renvoie la grappe coté serveur... il ne se rend pas compte qu'il y a eu suppression même avec les options type delete-orphan all-delete-orphan dans le mapping... La seule solution, l'ultime chance restante consiste à mapper la liste d'une manière bien différente.

Ce problème commun est connu sur le site hibernate.org :

------------------------------
I removed an object from a collection mapped with cascade="all" but the object was not deleted!

cascade="all" cascades the delete() operation from parent to child. If this is a one-to-many association, try using cascade="all,delete-orphan".

Another solution is to model your child objects as composite elements (a kind of value type) rather than entities. Value types are always persisted or removed along with their parent entity. So you would use a mapping for the element class instead of a mapping..
------------------------------

Voici le mapping de la liste contenu dans mon objet Truc :

<set name="maListe" table="element" lazy="true" >
<key column="elt_trc_id" not-null="true" />
<composite-element class="org.truc.Element">
<parent name="truc"/>

<property name="identifiant" type="java.lang.Integer">
<column name="elt_id" />
</property>

<property name="commentaire" type="java.lang.String">
<column name="elt_commentaire" length="254" />
</property>

<property name="date" type="timestamp">
<column name="elt_date" />
</property>

</composite-element>
</set>

au lieu de par exemple:

<list name="maListe" lazy="true" cascade="persist, save-update, evict, merge, all-delete-orphan">
<key column="elt_trc_id" not-null="true" />
<index column="elt_numero" />
<one-to-many class="Element" />
</list>

Bon les désavantages sont flagrants, on est obligé de mapper la liste dans le fichier de mapping de l'objet conteneur et dans le cas ou les objets de la liste sont utilisés dans plusieurs objets... on duplique on duplique... pas très pro comme on dit alors mieux vaut éviter.

Mais bon ca fonctionne et les objets sont biens mis à jours et surtout supprimés quand on sauvegarde l'objet conteneur...