Hibernate, filtrage des données enfants
Filtrer les données enfants... ça m'aura bien pris la tête...
Le topo : dans une base de données, on a rarement des tables sans liaisons... dans la plupart des cas, une table est liée à une autre, avec une relation un-à-un, un-à-plusieurs ou plusieurs-à-un, et plus rarement des liaisons plusieurs-à-plusieurs.
Hibernate permet, à l'aide de ses mappings de données, d'établir tous ces types de relations. Dans mon cas précis, je possède 3 tables qui sont définies comme tel :
kind
kindid
kindgroup
kindweight
kind_lang
kindid
langabrev
kind_langname
lang
langabrev
langdesc
langlocale
Comme vous pouvez le voir, la table kind_lang est une table intermédiaire entre la table kind et la table lang. Vous pouvez l'imaginer, kind représente un "type". Il existe dans l'application différents type de données, tels que : type d'acteur, type d'utilisateur, type de média, type de film, etc...
L'application étant multilingue, la table lang représente les langages disponibles dans l'application.
Enfin, la table kind_lang, vous vous en doutez, permet d'associer un langage avec un type de données. Elle contient les traductions pour chaque type de données.
Maintenant que le décor est posé, quelques explications sur Hibernate. Hibernate permet de faire du mapping de classes avec la base de données. On crée ce qu'on appelle des VO, plus communément nommés des Value Object, qui servent à stocker une ligne de données (par exemple une ligne d'une table de la base de données). Ces classes sont de simples objets typés, avec des accesseurs (getters/setters). On crée ensuite un fichier de mapping en format XML, qui permet d'associer notre classe avec une table de la base de données.
Dans les cas les plus simples, celui qui ne contient pas d'association spécifique, la création est simple, voici un exemple :
Admettons une table "format" contenant une valeur d'identifiant et une valeur de nommage. On pourrait définir la classe Java comme tel :
FormatVO.java
package ch.titouille.app.vo; public class FormatVO { private int id; private String name; /** * @return the id */ public int getId() { return id; } /** * @param id the id to set */ public void setId(int id) { this.id = id; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } }
On défini ensuite le fichier de mapping xml comme suivant :
FormatVO.hbm.xml
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="ch.titouille.app.vo"> <class name="FormatVO" table="format"> <id name="id" column="formatid" type="int" unsaved-value="0"> <generator class="native" /> </id> <property name="name" column="formatname" /> </class> <query name="all.formats">From FormatVO</query> </hibernate-mapping>
A priori, rien de compliqué. On peut constater que le fichier de mapping possède 2 champs, un champs identifiant "id" lié à la colonne "formatid" de la table format, et un champs "name" lié à la colonne "formatname" de la même table format. Ok, jusqu'ici, tout va bien, c'est un mapping tout à fait basique.
Maintenant, revenons à nos tables kind, kind_lang et langabrev.
Le but est de récupérer les types d'un groupe précis, tout en permettant de filtrer par rapport à un langage précis... imaginons que si la base de données supporte 5 langages différents, je ne veux pas qu'il me retourne 5 fois plus de données que je ne veux en exploiter dans mon application...
Pour ce faire, je crée d'abord mes classes. Il y a quelques subtilités à connaitre pour ce genre de mapping.
Tout d'abord, la classe kind_lang possède une clé composée. Cette clé composée doit être déclarée en tant que classe dans Java. Je crée donc d'abord une classe KindLangId qui va correspondre à cette clé composée :
package ch.titouille.app.vo; import java.io.Serializable; public class KindLangId implements Serializable { private int id; private String abrev; /** * @return the id */ public int getId() { return id; } /** * @param id the id to set */ public void setId(int id) { this.id = id; } /** * @return the abrev */ public String getAbrev() { return abrev; } /** * @param abrev the abrev to set */ public void setAbrev(String abrev) { this.abrev = abrev; } }
Vous pouvez voir que cette première classe implémente ISerializable. Ceci est une contrainte qu'impose Hibernate. Une classe représentant une clé composée doit obligatoirement implémenter la classe Serializable.
Passons. Maintenant que ma classe de clé composée est créée, je vais pouvoir passer à la suite. Tout d'abord, je vais créer la classe la plus basique, celle qui correspond à la table kind_lang. Cette classe étant une classe "intermédiaire", elle ne possèdera pas de mapping complexe. Je la défini comme suivant :
KindLangVO.java
package ch.titouille.app.vo; public class KindLangVO { private KindLangId id; private String name; /** * @return the id */ public KindLangId getId() { return id; } /** * @param id the id to set */ public void setId(KindLangId id) { this.id = id; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } }
cette fois encore, rien de très spécial, mis à part que l'identifiant correspond à ma classe de clé composée définie auparavant.
Je dois maintenant créer le fichier de mapping correspondant pour permettre d'associer la classe à la table de la base de données :
KindLangVO.hbm.xml
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="ch.titouille.app.vo"> <class name="KindLangVO" table="kind_lang"> <composite-id name="id" class="KindLangId"> <key-property name="id" type="int"> <column name="kindid" /> </key-property> <key-property name="abrev"> <column name="langabrev" /> </key-property> </composite-id> <property name="name" column="kind_langname"/> </class> </hibernate-mapping>
Ici, on peut voir que la clé de la table est une "composite-id" qui correspond à la classe définie au départ, "KindLangId". Les 2 propriétés de colonnes sont définies, et enfin, la propriété name, reliée à la colonne kind_langname.
C'est maintenant qu'on passe aux choses sérieuses. Une liaison dans un mapping hibernate se fait généralement avec une instruction "set". Il existe différentes collections utilisables avec Hibernate. Les "Set" sont les plus simples, mais il est possible d'utiliser des "List" (éléments ordonnés), des "Bag" (possibilité d'avoir des éléments dupliqués), et des "Map" (paires clé-valeur). Pour en revenir à l'exemple, il est donc nécessaire de créer une variable privée de type "Set" afin de pouvoir stocker les informations liées. Voici d'abord la déclaration de ma classe KindVO :
KindVO.java
/** * @author titouille * @creation 11 juin 08 19:58:47 * @version 1.0 */ package ch.titouille.app.vo; import java.util.Set; /** * @author titouille * */ public class KindVO { private int id; private Set name; private int weight; private int group; /** * @return the id */ public int getId() { return id; } /** * @param id the id to set */ public void setId(int id) { this.id = id; } /** * @return the name */ public Set getName() { return name; } /** * @param name the name to set */ public void setName(Set name) { this.name = name; } /** * @return the weight */ public int getWeight() { return weight; } /** * @param weight the weight to set */ public void setWeight(int weight) { this.weight = weight; } /** * @return the group */ public int getGroup() { return group; } /** * @param group the group to set */ public void setGroup(int group) { this.group = group; } }
Comme vous pouvez le voir la propriété "name" est de type Set, ce qui permet de stocker une liste de données, ces données provenant directement de la table kind_lang.
Seulement, comme je l'ai expliqué au départ, mon but est de pouvoir récupérer les données d'un seul langage, celui de l'utilisateur qui en fait la demande. La particularité est donc de pouvoir passer en paramètre le langage de l'utilisateur.
Dans un mapping xml, un Set se représente avec une balise "set", et peut prendre en compte un attribut "where", qui permet d'appliquer un filtre sur les données... Seulement, impossible de passer dynamiquement une valeur dans ce filtre... le seul moyen d'utiliser la clause "where" est de placer statiquement la valeur qu'on désire pour le filtre... Après quelques recherches sur le net, et dans le livre sur hibernate que je viens d'acquérir (Java persistance with Hibernate) j'ai enfin trouvé une solution qui fonctionne, et qu'on nomme : les filtres.
Le but est de définir un filtre dans le mapping XML, puis d'utiliser ce filtre en tant que filtre de la requête qui sera utilisée pour le set. Enfin, on ajoutera le paramètre du filtre dans la session hibernate qui est utilisée du côté de l'assembleur Java.
Voici donc la finalité du système :
KindVO.hbm.xml
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="ch.titouille.app.vo"> <class name="KindVO" table="kind"> <id name="id" column="kindid" type="int" unsaved-value="0"> <generator class="native" /> </id> <property name="weight" column="kindweight" /> <property name="group" column="kindgroup" /> <set name="name" table="kind_lang" lazy="false"> <key column="kindid" /> <one-to-many class="KindLangVO" /> <filter name="filterLanguage" condition="langabrev=:langabrev" /> </set> </class> <filter-def name="filterLanguage"> <filter-param name="langabrev" type="string" /> </filter-def> </hibernate-mapping>
Et dans mon assembleur Java, l'utilisation se fait de la manière suivante :
// get the current session Session s = HibernateUtil.getSessionFactory().getCurrentSession(); // create transaction, set query/params and commit transaction s.beginTransaction(); Filter filter = s.enableFilter("filterLanguage"); filter.setParameter("langabrev", "en"); List l = s.createQuery("From KindVO where group=:group") .setInteger("group", groupid ) .list(); s.getTransaction().commit(); return l;
Comme vous pouvez le voir, après avoir récupéré l'objet Session qui sera utilisé pour exécuter des transactions, j'initialise une transaction et j'ajoute à la session le filtre nécessaire, ainsi que son paramètre, avant d'effectuer la transaction à proprement dit. C'est de cette manière que je peux passer des paramètres de filtres à mes fichiers hbm.
Tout simplement... Pas si simple à trouver l'information, mais ça fonctionne, et c'est l'essentiel
Avec cette nouvelle découverte, je vais pouvoir aller de l'avant dans mes développements et créer des mappings de plus en plus complexes tout en ayant la possibilité de filtrer un maximum mes données.