1.2. Partie 1 - Première application Hibernate

Let's assume we need a small database application that can store events we want to attend, and information about the host(s) of these events. We will use an in-memory, Java database named HSQLDB to avoid describing installation/setup of any particular database servers. Feel free to tweak this tutorial to use whatever database you feel comfortable using.

The first thing we need to do is set up our development environment, and specifically to setup all the required dependencies to Hibernate as well as other libraries. Hibernate is built using Maven which amongst other features provides dependecy management; moreover it provides transitive dependecy management which simply means that to use Hibernate we can simply define our dependency on Hibernate, Hibernate itself defines the dependencies it needs which then become transitive dependencies of our project.

.
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    ...

    <dependencies>
        <dependency>
            <groupId>${groupId}</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>

        <!-- Because this is a web app, we also have a dependency on the servlet api. -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
        </dependency>
    </dependencies>

</project>

Note

Essentially we are describing here the /tutorials/web/pom.xml file. See the Maven site for more information.

Astuce

While not strictly necessary, most IDEs have integration with Maven to read these POM files and automatically set up a project for you which can save lots of time and effort.

Ensuite, nous créons une classe qui réprésente l'événement que nous voulons stocker dans notre base de données.

1.2.1. La première classe

Notre première classe persistante est une simple classe JavaBean avec quelques propriétés :

package org.hibernate.tutorial.domain;

import java.util.Date;

public class Event {
    private Long id;

    private String title;
    private Date date;

    public Event() {}

    public Long getId() {
        return id;
    }

    private void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

Vous pouvez voir que cette classe utilise les conventions de nommage standard JavaBean pour les méthodes getter/setter des propriétés, ainsi qu'une visibilité privée pour les champs. Ceci est la conception recommandée - mais pas obligatoire. Hibernate peut aussi accéder aux champs directement, le bénéfice des méthodes d'accès est la robustesse pour la refonte de code. Le constructeur sans argument est requis pour instancier un objet de cette classe via reflexion.

La propriété id contient la valeur d'un identifiant unique pour un événement particulier. Toutes les classes d'entités persistantes (ainsi que les classes dépendantes de moindre importance) auront besoin d'une telle propriété identifiante si nous voulons utiliser l'ensemble complet des fonctionnalités d'Hibernate. En fait, la plupart des applications (surtout les applications web) ont besoin de distinguer des objets par des identifiants, donc vous devriez considérer ça comme une fonctionnalité plutôt que comme une limitation. Cependant, nous ne manipulons généralement pas l'identité d'un objet, dorénavant la méthode setter devrait être privée. Seul Hibernate assignera les identifiants lorsqu'un objet est sauvegardé. Vous pouvez voir qu'Hibernate peut accéder aux méthodes publiques, privées et protégées, ainsi qu'aux champs (publics, privés, protégés) directement. Le choix vous est laissé, et vous pouvez l'ajuster à la conception de votre application.

Le constructeur sans argument est requis pour toutes les classes persistantes ; Hibernate doit créer des objets pour vous en utilisant la réflexion Java. Le constructeur peut être privé, cependant, la visibilité du paquet est requise pour la génération de proxy à l'exécution et une récupération des données efficaces sans instrumentation du bytecode.

Placez ce fichier source Java dans un répertoire appelé src dans le dossier de développement. Ce répertoire devrait maintenant ressembler à ça :

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java

Dans la prochaine étape, nous informons Hibernate de cette classe persistante.

1.2.2. Le fichier de mapping

Hibernate a besoin de savoir comment charger et stocker des objets d'une classe persistante. C'est là qu'intervient le fichier de mapping Hibernate. Le fichier de mapping indique à Hibernate à quelle table dans la base de données il doit accéder, et quelles colonnes de cette table il devra utiliser.

La structure basique de ce fichier de mapping ressemble à ça :

<?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>
[...]
</hibernate-mapping>

Notez que la DTD Hibernate est très sophistiquée. Vous pouvez l'utiliser pour l'auto-complétement des éléments et des attributs de mapping XML dans votre éditeur ou votre IDE. Vous devriez aussi ouvrir le fichier DTD dans votre éditeur de texte - c'est le moyen le plus facile d'obtenir une vue d'ensemble de tous les éléments et attributs, et de voir les valeurs par défaut, ainsi que quelques commentaires. Notez qu'Hibernate ne chargera pas le fichier DTD à partir du web, mais regardera d'abord dans le classpath de l'application. Le fichier DTD est inclus dans hibernate3.jar ainsi que dans le répertoire src de la distribution Hibernate.

Nous omettrons la déclaration de la DTD dans les exemples futurs pour raccourcir le code. Bien sûr il n'est pas optionnel.

Entre les deux balises hibernate-mapping, incluez un élément class. Toutes les classes d'entités persistantes (encore une fois, il pourrait y avoir des classes dépendantes plus tard, qui ne sont pas des entités mère) ont besoin d'un mapping vers une table de la base de données SQL :

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">

    </class>

</hibernate-mapping>

Plus loin, nous disons à Hibernate comment persister et charger un objet de la classe Event dans la table EVENTS, chaque instance est représentée par une ligne dans cette table. Maintenant nous continuons avec le mapping de la propriété de l'identifiant unique vers la clef primaire de la table. De plus, comme nous ne voulons pas nous occuper de la gestion de cet identifiant, nous utilisons une stratégie de génération d'identifiant d'Hibernate pour la colonne de la clef primaire subrogée :

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">
        <id name="id" column="EVENT_ID">
            <generator class="native"/>
        </id>
    </class>

</hibernate-mapping>

The id element is the declaration of the identifier property, name="id" declares the name of the Java property - Hibernate will use the getter and setter methods to access the property. The column attribute tells Hibernate which column of the EVENTS table we use for this primary key. The nested generator element specifies the identifier generation strategy, in this case we used native, which picks the best strategy depending on the configured database (dialect). Hibernate supports database generated, globally unique, as well as application assigned identifiers (or any strategy you have written an extension for).

Finalement nous incluons des déclarations pour les propriétés persistantes de la classe dans le fichier de mapping. Par défaut, aucune propriété de la classe n'est considérée comme persistante :

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">
        <id name="id" column="EVENT_ID">
            <generator class="native"/>
        </id>
        <property name="date" type="timestamp" column="EVENT_DATE"/>
        <property name="title"/>
    </class>

</hibernate-mapping>

Comme avec l'élément id, l'attribut name de l'élément property indique à Hibernate quels getters/setters utiliser.

Pourquoi le mapping de la propriété date inclut l'attribut column, mais pas title ? Sans l'attribut column Hibernate utilise par défaut le nom de la propriété comme nom de colonne. Ca fonctionne bien pour title. Cependant, date est un mot clef réservé dans la plupart des bases de données, donc nous utilisons un nom différent pour le mapping.

La prochaine chose intéressante est que le mapping de title manque aussi d'un attribut type. Les types que nous déclarons et utilisons dans les fichiers de mapping ne sont pas, comme vous pourriez vous y attendre, des types de données Java. Ce ne sont pas, non plus, des types de base de données SQL. Ces types sont donc appelés des types de mapping Hibernate, des convertisseurs qui peuvent traduire des types Java en types SQL et vice versa. De plus, Hibernate tentera de déterminer la bonne conversion et le type de mapping lui-même si l'attribut type n'est pas présent dans le mapping. Dans certains cas, cette détection automatique (utilisant la réflexion sur la classe Java) pourrait ne pas donner la valeur attendue ou dont vous avez besoin. C'est le cas avec la propriété date. Hibernate ne peut pas savoir si la propriété "mappera" une colonne SQL de type date, timestamp ou time. Nous déclarons que nous voulons conserver des informations avec une date complète et l'heure en mappant la propriété avec un timestamp.

Ce fichier de mapping devrait être sauvegardé en tant que Event.hbm.xml, juste dans le répertoire à côté du fichier source de la classe Java Event. Le nommage des fichiers de mapping peut être arbitraire, cependant le suffixe hbm.xml est devenu une convention dans la communauté des développeurs Hibernate. La structure du répertoire devrait ressembler à ça :

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml

Nous poursuivons avec la configuration principale d'Hibernate.

1.2.3. Configuration d'Hibernate

Nous avons maintenant une classe persistante et son fichier de mapping. Il est temps de configurer Hibernate. Avant ça, nous avons besoin d'une base de données. HSQL DB, un SGBD SQL basé sur Java et travaillant en mémoire, peut être téléchargé à partir du site web de HSQL. En fait, vous avez seulement besoin de hsqldb.jar. Placez ce fichier dans le répertoire lib/ du dossier de développement.

Créez un répertoire appelé data à la racine du répertoire de développement - c'est là que HSQL DB stockera ses fichiers de données. Démarrez maintenant votre base de données en exécutant java -classpath lib/hsqldb.jar org.hsqldb.Server dans votre répertoire de travail. Vous observez qu'elle démarre et ouvre une socket TCP/IP, c'est là que notre application se connectera plus tard. Si vous souhaitez démarrez à partir d'une nouvelle base de données pour ce tutoriel (faites CTRL + C dans la fenêtre the window), effacez le répertoire data/ et redémarrez HSQL DB à nouveau.

Hibernate est la couche de votre application qui se connecte à cette base de données, donc il a besoin des informations de connexion. Les connexions sont établies à travers un pool de connexions JDBC, que nous devons aussi configurer. La distribution Hibernate contient différents outils de gestion de pools de connexions JDBC open source, mais pour ce didacticiel nous utiliserons le pool de connexions intégré à Hibernate. Notez que vous devez copier les bibliothèques requises dans votre classpath et utiliser une configuration de pool de connexions différente si vous voulez utiliser un logiciel de gestion de pools JDBC tiers avec une qualité de production.

Pour la configuration d'Hibernate, nous pouvons utiliser un simple fichier hibernate.properties, un fichier hibernate.cfg.xml légèrement plus sophistiqué, ou même une configuration complète par programmation. La plupart des utilisateurs préfèrent le fichier de configuration XML :

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

    <session-factory>

        <!-- Database connection settings -->
        <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="connection.url">jdbc:hsqldb:hsql://localhost</property>
        <property name="connection.username">sa</property>
        <property name="connection.password"></property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">1</property>

        <!-- SQL dialect -->
        <property name="dialect">org.hibernate.dialect.HSQLDialect</property>

        <!-- Enable Hibernate's automatic session context management -->
        <property name="current_session_context_class">thread</property>

        <!-- Disable the second-level cache  -->
        <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create</property>

        <mapping resource="events/Event.hbm.xml"/>

    </session-factory>

</hibernate-configuration>

Notez que cette configuration XML utilise une DTD différente. Nous configurons une SessionFactory d'Hibernate - une fabrique globale responsable d'une base de données particulière. Si vous avez plusieurs base de données, utilisez plusieurs configurations <session-factory>, généralement dans des fichiers de configuration différents (pour un démarrage plus facile).

Les quatre premiers éléments property contiennent la configuration nécessaire pour la connexion JDBC. L'élément property du dialecte spécifie quelle variante du SQL Hibernate va générer. La gestion automatique des sessions d'Hibernate pour les contextes de persistance sera détaillée très vite. L'option hbm2ddl.auto active la génération automatique des schémas de base de données - directement dans la base de données. Cela peut bien sûr aussi être désactivé (en supprimant l'option de configuration) ou redirigé vers un fichier avec l'aide de la tâche Ant SchemaExport. Finalement, nous ajoutons le(s) fichier(s) de mapping pour les classes persistantes.

Copiez ce fichier dans le répertoire source, il terminera dans la racine du classpath. Hibernate cherchera automatiquement, au démarrage, un fichier appelé hibernate.cfg.xml dans la racine du classpath.

1.2.4. Construction avec Ant

Nous allons maintenant construire le didacticiel avec Ant. Vous aurez besoin d'avoir Ant d'installé - récupérez-le à partir de la page de téléchargement de Ant. Comment installer Ant ne sera pas couvert ici. Référez-vous au manuel d'Ant. Après que vous aurez installé Ant, nous pourrons commencer à créer le fichier de construction. Il s'appellera build.xml et sera placé directement dans le répertoire de développement.

Un fichier de construction basique ressemble à ça :

<project name="hibernate-tutorial" default="compile">

    <property name="sourcedir" value="${basedir}/src"/>
    <property name="targetdir" value="${basedir}/bin"/>
    <property name="librarydir" value="${basedir}/lib"/>

    <path id="libraries">
        <fileset dir="${librarydir}">
            <include name="*.jar"/>
        </fileset>
    </path>

    <target name="clean">
        <delete dir="${targetdir}"/>
        <mkdir dir="${targetdir}"/>
    </target>

    <target name="compile" depends="clean, copy-resources">
      <javac srcdir="${sourcedir}"
             destdir="${targetdir}"
             classpathref="libraries"/>
    </target>

    <target name="copy-resources">
        <copy todir="${targetdir}">
            <fileset dir="${sourcedir}">
                <exclude name="**/*.java"/>
            </fileset>
        </copy>
    </target>

</project>

Cela dira à Ant d'ajouter tous les fichiers du répertoire lib finissant par .jar dans le classpath utilisé pour la compilation. Cela copiera aussi tous les fichiers source non Java dans le répertoire cible, par exemple les fichiers de configuration et de mapping d'Hibernate. Si vous lancez Ant maintenant, vous devriez obtenir cette sortie :

C:\hibernateTutorial\>ant
Buildfile: build.xml

copy-resources:
     [copy] Copying 2 files to C:\hibernateTutorial\bin

compile:
    [javac] Compiling 1 source file to C:\hibernateTutorial\bin

BUILD SUCCESSFUL
Total time: 1 second 

1.2.5. Démarrage et aides

Il est temps de charger et de stocker quelques objets Event, mais d'abord nous devons compléter la configuration avec du code d'infrastructure. Nous devons démarrer Hibernate. Ce démarrage inclut la construction d'un objet SessionFactory global et le stocker quelque part facile d'accès dans le code de l'application. Une SessionFactory peut ouvrir des nouvelles Sessions. Une Session représente une unité de travail simplement "threadée", la SessionFactory est un objet global "thread-safe", instancié une seule fois.

Nous créerons une classe d'aide HibernateUtil qui s'occupe du démarrage et rend la gestion des Sessions plus facile. Regardons l'implémentation :

package util;

import org.hibernate.*;
import org.hibernate.cfg.*;

public class HibernateUtil {

    private static final SessionFactory sessionFactory;

    static {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

}

Cette classe ne produit pas seulement la SessionFactory globale dans un initialiseur statique (appelé une seule fois par la JVM lorsque la classe est chargée), elle masque le fait qu'elle exploite un singleton. Elle pourrait aussi obtenir la SessionFactory depuis JNDI dans un serveur d'applications.

Si vous nommez la SessionFactory dans votre fichier de configuration, Hibernate tentera la récupération depuis JNDI. Pour éviter ce code, vous pouvez aussi utiliser un déploiement JMX et laisser le conteneur (compatible JMX) instancier et lier un HibernateService à JNDI. Ces options avancées sont détaillées dans la documentation de référence Hibernate.

Placez HibernateUtil.java dans le répertoire source de développement, et ensuite Event.java :

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml
  +util
    HibernateUtil.java
  hibernate.cfg.xml
+data
build.xml

Cela devrait encore compiler sans problème. Nous avons finalement besoin de configurer le système de "logs" - Hibernate utilise commons-logging et vous laisse le choix entre log4j et le système de logs du JDK 1.4. La plupart des développeurs préfèrent log4j : copiez log4j.properties de la distribution d'Hibernate (il est dans le répertoire etc/) dans votre répertoire src, puis faites de même avec hibernate.cfg.xml. Regardez la configuration d'exemple et changez les paramètres si vous voulez une sortie plus verbeuse. Par défaut, seul le message de démarrage d'Hibernate est affiché sur la sortie standard.

L'infrastructure de ce didacticiel est complète - et nous sommes prêts à effectuer un travail réel avec Hibernate.

1.2.6. Charger et stocker des objets

Finalement nous pouvons utiliser Hibernate pour charger et stocker des objets. Nous écrivons une classe EventManager avec une méthode main() :

package events;
import org.hibernate.Session;

import java.util.Date;

import util.HibernateUtil;

public class EventManager {

    public static void main(String[] args) {
        EventManager mgr = new EventManager();

        if (args[0].equals("store")) {
            mgr.createAndStoreEvent("My Event", new Date());
        }

        HibernateUtil.getSessionFactory().close();
    }

    private void createAndStoreEvent(String title, Date theDate) {

        Session session = HibernateUtil.getSessionFactory().getCurrentSession();

        session.beginTransaction();

        Event theEvent = new Event();
        theEvent.setTitle(title);
        theEvent.setDate(theDate);

        session.save(theEvent);

        session.getTransaction().commit();
    }

}

Nous créons un nouvel objet Event, et le remettons à Hibernate. Hibernate s'occupe maintenant du SQL et exécute les INSERTs dans la base de données. Regardons le code de gestion de la Session et de la Transaction avant de lancer ça.

Une Session est une unité de travail. Pour le moment, nous allons faire les choses simplement et assumer une granularité un-un entre une Session hibernate et une transaction à la base de données. Pour isoler notre code du système de transaction sous-jacent (dans notre cas, du pure JDBC, mais cela pourrait être JTA), nous utilisons l'API Transaction qui est disponible depuis la Session Hibernate.

Que fait sessionFactory.getCurrentSession() ? Premièrement, vous pouvez l'invoquer autant de fois que vous le voulez et n'importe où du moment que vous avez votre SessionFactory (facile grâce à HibernateUtil). La méthode getCurrentSession() renvoie toujours l'unité de travail courante. Souvenez vous que nous avons basculé notre option de configuration au mécanisme basé sur le "thread" dans hibernate.cfg.xml. Par conséquent, le scope de l'unité de travail courante est le thread java courant d'exécution. Ceci n'est pas totalement vrai.

Une Session commence lorsqu'elle est vraiment utilisée la première fois, Lorsque nous appelons pour la première fois getCurrentSession(). Ensuite, elle est liée, par Hibernate, au thread courant. Lorsque la transaction s'achève (commit ou rollback), Hibernate délie la Session du thread et la ferme pour vous. Si vous invoquez getCurrentSession() une autre fois, vous obtenez une nouvelle Session et pouvez entamer une nouvelle unité de travail. Ce modèle de programmation "thread-bound" est le moyen le plus populaire d'utiliser Hibernate.

UNTRANSLATED ! Related to the unit of work scope, should the Hibernate Session be used to execute one or several database operations? The above example uses one Session for one operation. This is pure coincidence, the example is just not complex enough to show any other approach. The scope of a Hibernate Session is flexible but you should never design your application to use a new Hibernate Session for every database operation. So even if you see it a few more times in the following (very trivial) examples, consider session-per-operation an anti-pattern. A real (web) application is shown later in this tutorial.

Lisez Chapitre 11, Transactions et accès concurrents pour plus d'informations sur la gestion des transactions et leur démarcations. Nous n'avons pas géré les erreurs et rollback sur l'exemple précédent.

Pour lancer cette première routine, nous devons ajouter une cible appelable dans le fichier de construction de Ant :

<target name="run" depends="compile">
    <java fork="true" classname="events.EventManager" classpathref="libraries">
        <classpath path="${targetdir}"/>
        <arg value="${action}"/>
    </java>
</target>

La valeur de l'argument action correspond à la ligne de commande qui appelle la cible :

C:\hibernateTutorial\>ant run -Daction=store

Vous devriez voir, après la compilation, Hibernate démarrer et, en fonction de votre configuration, beaucoup de traces sur la sortie. À la fin vous trouverez la ligne suivante :

[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)

C'est l'INSERT exécuté par Hibernate, les points d'interrogation représentent les paramètres JDBC liés. Pour voir les valeurs liées aux arguments, ou pour réduire la verbosité des traces, vérifier votre log4j.properties.

Maintenant nous aimerions aussi lister les événements stockés, donc nous ajoutons une option à la méthode principale :

if (args[0].equals("store")) {
    mgr.createAndStoreEvent("My Event", new Date());
}
else if (args[0].equals("list")) {
    List events = mgr.listEvents();
    for (int i = 0; i < events.size(); i++) {
        Event theEvent = (Event) events.get(i);
        System.out.println("Event: " + theEvent.getTitle() +
                           " Time: " + theEvent.getDate());
    }
}

Nous ajoutons aussi une nouvelle méthode listEvents() :

private List listEvents() {

    Session session = HibernateUtil.getSessionFactory().getCurrentSession();

    session.beginTransaction();

    List result = session.createQuery("from Event").list();

    session.getTransaction().commit();

    return result;
}

Ce que nous faisons ici c'est utiliser une requête HQL (Hibernate Query Language) pour charger tous les objets Event existants de la base de données. Hibernate générera le SQL approprié, l'enverra à la base de données et peuplera des objets Event avec les données. Vous pouvez créer des requêtes plus complexes avec HQL, bien sûr.

Maintenant, pour exécuter et tester tout ça, suivez ces étapes :

  • Exécutez ant run -Daction=store pour stocker quelque chose dans la base de données et, bien sûr, pour générer, avant, le schéma de la base de données grâce à hbm2ddl.

  • Now disable hbm2ddl by commenting out the property in your hibernate.cfg.xml file. Usually you only leave it turned on in continuous unit testing, but another run of hbm2ddl would drop everything you have stored - the create configuration setting actually translates into "drop all tables from the schema, then re-create all tables, when the SessionFactory is build".

Si maintenant vous appelez Ant avec -Daction=list, vous devriez voir les événements que vous avez stockés jusque là. Vous pouvez bien sûr aussi appeler l'action store plusieurs fois.

UNTRANSLATED! Note: Most new Hibernate users fail at this point and we see questions about Table not found error messages regularly. However, if you follow the steps outlined above you will not have this problem, as hbm2ddl creates the database schema on the first run, and subsequent application restarts will use this schema. If you change the mapping and/or database schema, you have to re-enable hbm2ddl once again.