Génération de classes modèles avec Hibernate Tools

Une des activités chronophages lorsqu’on travaille avec Hibernate est de créer les classes modèles ainsi que les fichiers de mappings associés. Or si vous êtes amenés à travailler sur un modèle physique de données préexistant, votre tâche sera simplifiée grâce au plugin eclipse “Hibernate Tools”. Ce plugin permet de faire du reverse engineering sur votre modèle physique afin de générer l’ensemble des artefacts utiles à la persistance de vos données (classes modèles, fichiers .hbm, DAO etc…). De plus vous pourrez contrôler finement cette génération à l’aide de fichiers de configurations. Dans cet article je m’attarderai principalement sur la génération de classes modèles avec annotations JPA.

Afin de vous montrer les possibilités qu’offre cet outil, je vais partir d’un modèle physique de données concret qui est le suivant :

Au final vous serez en mesure de générer une classe modèle comme suit :

/**
*
* My private copyright 2
*
*/
package fr.xebia.reveng.model;

// Generated test Mar 12, 2012 10:52:47 PM by Hibernate Tools 3.4.0.CR1

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import fr.xebia.reveng.model.common.IPojoWithCode;
import fr.xebia.reveng.model.store.StoreHasProduct;

/**
* Product generated by hbm2java
*/
@Entity
@Table(name = "T_PRODUCT", schema = "PUBLIC", catalog = "PUBLIC")
public class Product implements IPojoWithCode, java.io.Serializable {

    private int id;
    private String code;
    private String name;
    private Set<StoreHasProduct> storeHasProducts = new HashSet<StoreHasProduct>(0);

    public Product() {
    }

    public Product(int id, String code, String name) {
        this.id = id;
        this.code = code;
        this.name = name;
    }

    public Product(int id, String code, String name,
            Set<StoreHasProduct> storeHasProducts) {
        this.id = id;
        this.code = code;
        this.name = name;
        this.storeHasProducts = storeHasProducts;
    }

    @Id
    @Column(name = "ID", unique = true, nullable = false)
    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Column(name = "CODE", nullable = false, length = 50)
    public String getCode() {
        return this.code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Column(name = "NAME", nullable = false, length = 50)
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH }, fetch = FetchType.LAZY, mappedBy = "product")
    public Set<StoreHasProduct> getStoreHasProducts() {
        return this.storeHasProducts;
    }

    public void setStoreHasProducts(Set<StoreHasProduct> storeHasProducts) {
        this.storeHasProducts = storeHasProducts;
    }

}

Custom reveng strategy

Une des pratiques en base de données relationnelle est de préfixer le nom des éléments de la base suivant leur type (T_ pour une table, V_ pour une vue …). Cependant lors du reverse engineering cela va nous donner des classes dont le nom sera TProduct ou TStore. Pas très joli me direz vous comme nom de classe je vous l’accorde. Pour y remédier Hibernate Tools permet de redéfinir la stratégie de reverse engineering. Pour ce faire il suffit de créer une classe, par exemple MyCustomRevengStrategy, qui hérite de org.hibernate.cfg.reveng.DelegatingReverseEngineeringStrategy. Il faut reconnaître que cette dernière n’est pas très bien documentée et il faut y aller à tâtons pour voir comment elle fonctionne. La méthode tableToClassname est celle responsable de génerer le nom d’une classe Java à partir d’un nom de table en base. Un simple override de cette méthode va permettre de supprimer ce préfixe qui nous gêne :

private static final String TABLE_PREFIX = "T_";

@Override
public String tableToClassName(TableIdentifier tableIdentifier) {
 String defaultName = super.tableToClassName(tableIdentifier);

 String tableName = tableIdentifier.getName();
 String qualifiedClassName = defaultName;
 if (tableName.startsWith(TABLE_PREFIX)) {
     qualifiedClassName = retrieveQualifiedClassNameWithoutTablePrefix(defaultName);
 }

 return qualifiedClassName;
}

private String retrieveQualifiedClassNameWithoutTablePrefix(
     String qualifiedClassName) {

 if (Strings.isNullOrEmpty(qualifiedClassName)) {
     return qualifiedClassName;
 }

 return removeTablePrefixOfQualifiedClassName(qualifiedClassName);
}

private String removeTablePrefixOfQualifiedClassName(
     String qualifiedClassNameWithPrefix) {
 int indexOfLastDot = qualifiedClassNameWithPrefix.lastIndexOf(".");
 String classNameWithoutPrefix = qualifiedClassNameWithPrefix.substring(
  indexOfLastDot + 2, qualifiedClassNameWithPrefix.length());
 String packageName = qualifiedClassNameWithPrefix.substring(0,
  indexOfLastDot + 1);
 return packageName + classNameWithoutPrefix;
}

Ensuite dans la console de configuration, il ne reste plus qu’ à faire pointer le champs reveng.strategy vers notre nouvelle stratégie. Maintenant nous avons des noms de classes propres à manipuler, mais la customisation ne s’arrête pas là.

Reveng.xml

Ce fichier permet de contrôler plus finement la génération des classes modèles, il peut être créé à la main mais vous pouvez aussi utiliser l’éditeur du plugin pour faciliter sa génération.

Filtrage des tables

Lorsque le nombre de classes modèles devient réellement important, il peut être utile de l’organiser à l’aide d’une structure de packages adaptée. Hibernate tools permet alors de générer les entités dans leurs packages associés en se servant de règles de nommage :

<table-filter match-catalog="PUBLIC" match-schema="PUBLIC" match-name="T_STORE.*" package="fr.xebia.reveng.model.store" />

Dans l’exemple ci-dessus, on se sert de l’attribut match-name pour définir le pattern des noms des tables concernées. Ici toutes les tables dont le nom commence par T_STORE verront leurs classes modèles générées dans le package fr.xebia.reveng.model.store.

Mapping de type

Cette section permet de préciser comment un type JDBC doit être converti en java. Un des meilleurs exemples est le cas de colonnes dont le type JDBC est VARCHAR(1), en effet il est préférable de convertir ce dernier en char plutôt qu’en String (utilisée par défaut pour le type VARCHAR). On peut donc préciser le mapping suivant :

<type-mapping>
    <sql-type jdbc-type="VARCHAR" length="1" hibernate-type="char" not-null="true" />
    <sql-type jdbc-type="VARCHAR" length="1" not-null="true" hibernate-type="java.lang.Character" />
</type-mapping>

Il y a 2 choses à remarquer dans cet exemple :

  1. Les déclarations de mapping vont généralement de pair pour prendre en compte le cas de nullité.
  2. L’utilisation de la propriété length qui précise que cette règle ne s’applique qu’au VARCHAR de longueur 1. Il existe 2 autres propriétés scale et precision qui quant à elles sont utiles pour les types numériques.

Renommage de variables

Il arrive que des champs en base de données aient des noms de colonne dont la signification échappe au sens commun, il est alors parfois bon de pouvoir les renommer. Dans notre cas il s’agit du champs XYZ de la table T_STORE qui en réalité représente le nombre de rayons du magasin. Pour le renommer, il faut ajouter :

<table catalog="PUBLIC" schema="PUBLIC" name="T_STORE">
    <column name="XYZ" property="nbRayons" />
</table>

à notre fichier reveng et la propriété qui initialement aurait été xyz sera finalement renommée en nbRayons. A l’aide de cette balise il est aussi possible de définir au passage le type souhaité pour cette variable permettant ainsi de surcharger le comportement par défaut du type-mapping.

Implémentation d’interface

Imaginons que nous voulions filtrer nos produits et nos magasins suivant leur code à l’aide de guava. Afin de pouvoir créer un prédicat générique, on veut que ces 2 entités implémentent la même interface que nous nommerons IPojoWithCode. On souhaite maintenant qu’à la génération de notre modèle nos classes Product et Store implémentent cette interface. Pour ce faire il suffit d’ajouter :

<table catalog="PUBLIC" schema="PUBLIC" name="T_PRODUCT">
     <meta attribute="extra-import">fr.xebia.reveng.model.common.IPojoWithCode</meta>
     <meta attribute="implements">IPojoWithCode</meta>
</table>

La première balise indique que nous ajoutons un import dans la classe en cours de génération (celui de notre interface). La deuxième balise indique que nous voulons que notre entité implémente l’interface IPojoWithCode.

Ajout des cascades

Lorsqu’on travaille avec Hibernate on est très rapidement amené à travailler avec des collections d’objets persistants. Il est souvent utile de permettre à Hibernate de faire des opérations en cascade (lors d’un save par exemple). Nous allons faire en sorte que la collection storeHasProducts de la classe Product ait un cascadeType PERSIST et REFRESH. Avant de pouvoir faire ça, il faut connaître le nom de la contrainte qui lie les Tables T_PRODUCT et T_STORE_HAS_PRODUCT. Dans notre cas elle se nomme T_STORE_HAS_PRODUCT_PRODUCT_ID. La subtilité dans ce genre de relation est qu’il faut placer la configuration entre les balises de la table qui possède la contrainte, ici il s’agit de T_STORE_HAS_PRODUCT. A partir de ce moment, on obtient le résultat escompté en ajoutant la configuration suivante dans notre fichier reveng :

<table catalog="PUBLIC" schema="PUBLIC" name="T_STORE_HAS_PRODUCT">
    <foreign-key constraint-name="T_STORE_HAS_PRODUCT_PRODUCT_ID">
        <set cascade="PERSIST,REFRESH" />
    </foreign-key>
</table>

Pour aller plus loin

En regardant de plus près la console de configuration de la génération on remarque une checkbox “Use custom templates”. Cette option permet d’utiliser ses propres templates pour la génération des classes modèles.

Configuration

Pour avoir un exemple des templates utilisés par Hibernate tools, il suffit de dézipper le jar hibernate-tools présent dans le dossier plugins du répertoire d’installation d’Eclipse. Une fois dézippé, copier le dossier “pojo” dans un dossier “template” sous “src/main/resources” de votre projet. Au passage vous pourrez remarquer la présence des dossiers dao, hbm qui servent à la génération d’autres artefacts. Maintenant faites pointer le champs “Template directory” de la console de configuration vers votre dossier “template” fraîchement créé.

Les templates

Dans le dossier pojo, vous trouverez tout un tas de fichiers .ftl qui sont nos fameux templates. Si vous prenez la peine d’ouvrir un de ces fichiers vous vous retrouverez nez à nez avec ce genre d’expression :

<#if property.equals(clazz.identifierProperty)>
    ${pojo.generateAnnIdGenerator()}
    <#-- if this is the id property (getter)-->
    <#-- explicitly set the column name for this property-->
</#if>

Hibernate tools utilise en fait la librairie freemarker comme moteur de template. L’explication de ce moteur étant hors du cadre de cet article je ne peux que vous conseiller de faire un tour sur leur site.

Manipulation des templates des pojos

Nous allons faire un exemple classique de modification de template avec l’ajout d’un copyright sur nos classes modèles. Pour cela rien de plus simple, ajouter le code suivant en début du fichier pojo.ftl :

/**
*
* My private copyright 2
*
*/
${pojo.getPackageDeclaration()}

Et voilà à la prochaine génération toutes vos classes auront un copyright dans leur en-tête.

En conclusion, Hibernate tools peut permettre de gagner un temps non négligeable à l’heure de créer son modèle de données. Cependant je tiens à rappeler que le modèle généré ne fait que mapper les relations d’une base de données, le modèle obtenu n’est donc pas parfait et peut être amélioré. Il peut néanmoins servir d’un bon point de départ.

Liens utiles

7 Responses

  • Petite question : est-il possible de demander à Hibernate Tools d’utiliser Bean Validator pour annoter certains champs ?

  • De mémoire je ne crois pas qu’il dispose de cette fonctionnalité, mais avec des meta attributs et une manipulation des templates ça devrait être possible à mettre en place (à vérifier).

  • Pour avoir eu l’occasion d’utiliser cet outil par le passé, je suis pour ma part loin d’être enthousiasmé, car le niveau de reverse engineering est un peu bébête, et les tables purement relationnelles qui ont été créées dans la base de données pour faire état de relations nn entre deux objets de la base de données se retrouvent mappées en classes java ce qui est aberrant.

    Du coup il faut vraiment étudier l’outil en profondeur pour apprendre à le maitriser et au final on va aussi vite d’écrire soi-même les mappings, d’autant plus qu’on sait ce qu’on fait et à quel métier se trouve derrière le modèle de données.

    Mais c’est sympa de voir un article sur Hibernate, je serai curieux de savoir comment vous adressez les problématiques de forte charge.

  • Bonjour,

    Tout d’abord merci pour ce retour d’expérience,

    « les tables purement relationnelles qui ont été créées dans la base de données pour faire état de relations nn entre deux objets de la base de données se retrouvent mappées en classes java ce qui est aberrant » -> Je viens de tester ce cas et l’outil ne me crée pas de classe java pour la table de jointure mais bien une annotation du type (@ManyToMany @JoinTable). Cependant si une des colonnes de la table de jointure n’appartient pas à la clé primaire, il faut penser à l’exclure dans le fichier reveng si on ne veut pas générer la classe java associée.

    « Du coup il faut vraiment étudier l’outil en profondeur pour apprendre à le maitriser » -> Il est vrai que cela prend un peu de temps pour prendre en main l’outil surtout que la documentation n’est pas complète. Mais une fois maitrisé il peut réellement s’avérer utile mais malheureusement ne s’adapte pas à toutes les situations.

  • Pas tres utile, je trouve. Il y a des IDE parmi lesquels je citerai Netbeans qui font le même boulot en quelques click.

  • Il est vrai que je n’aie pas insisté dessus, que ce soit dans Eclipse ou NetBeans la plupart des customisations (mais pas toutes) peuvent se faire au travers d’une interface. Mais il est toujours utile de comprendre ce que décrit le fichier reveng.xml.

  • Cet article est super parce qu’il est très clair contrairement à pas mal de docs Hibernate et surtout parce que c’est pile poil ce que je cherchais : comment ajouter des cascades.

    Je cherche à désactiver la gestion par proxy de certains beans (en ajoutant l’annotation @Proxy(lazy=false) au début de la classe). Est-ce que vous savez si c’est possible ?

Laisser un commentaire