Looking for Computer Science  & Information Technology online courses ?
Check my new web site: https://www.yesik.it !

JPA (Java Persistence API) est l'interface standard dans Java pour assurer la persistance des données. Mais JPA n'est qu'une interface. Pour pouvoir l'utiliser il vous faut un fournisseur de service. Nous allons voir dans cet article comment utiliser Hibernate comme fournisseur de persistance JPA dans le contexte d'une application JavaSE.

Le projet initial

Puisqu'il faut un exemple, prenons en un très simple. Notre application va gérer les animaux d'un zoo. Ces animaux sont identifiés par leur nom (unique: ça sera notre clé primaire) et contiendra leur date d'entrée au zoo. Ce qui nous donne la classe Java suivante:

package fr.chicoree.jpa.zoo;
 
import java.util.Date;
 
public class Animal {
    public Animal(String nom, Date entrée) {
        this.nom = nom;
        this.entrée = entrée;
    }
 
    public String getNom() {
        return nom;
    }
 
    public Date getEntrée() {
        return (Date)entrée.clone();
    }
 
    public String toString() {
        return String.format("%s [%tF]", nom, entrée);
    }
 
    private String      nom;
    private Date        entrée;
}

Comme vous le voyez, cette classe n'a rien d'extraordinaire.

Accents

Si la présence d'accents dans les identifiants vous surprend, sachez que, même si c'est peu connu, c'est autorisé en Java! C'est pratique si vous utilisez des termes français comme identifiant. Mais un petit bémol cependant: faites bien attention que l'encodage des caractères utilisé par votre éditeur de texte et par le compilateur javac soit identique. Sinon, vous vous exposez à bien des désagréments...

Si vous le voulez, vous pouvez rapidement vous assurer du fonctionnent de cette classe à l'aide du programme suivant:

package fr.chicoree.jpa.zoo;
 
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
 
public class TestSansJPA {
    public static void main(String[] args) {
        Date entrée = new GregorianCalendar(2002, Calendar.DECEMBER, 3).getTime();
        Animal  animal = new Animal("Baloo", entrée);
 
        System.out.println(animal);
    }
}

Voilà toute l'étendue de notre exemple. Je suis d'accord: c'est un peu simpliste mais c'est très suffisant pour ce premier contact. Maintenant, tout le reste de cet article visera à utiliser JPA/Hibernate pour assurer la persistance des instances de la classe Animal. C'est à dire pouvoir enregistrer les animaux dans une base de donnée relationnelle – et pouvoir les recharger ultérieurement.

Premiers programmes avec JPA

Le code source

Plutôt que de tergiverser plus longtemps, nous allons tout de suite voir le code source de non pas un, mais deux programmes utilisant JPA. Le premier servira à créer des instances de la classe Animal et à les rendre persistantes. Le second rechargera et affichera l'ensemble des animaux stockés dans la base de données.

Donc voici le premier programme. Celui-ci attend sur la ligne de commande le nom des animaux à ajouter, instancie un objet pour chaque animal, et le rend persistant:

package fr.chicoree.jpa.zoo;
 
import javax.persistence.*;
import java.util.*;
 
public class AjouteAnimal {
    public static void main(String[] args) {
        // Démarre JPA et ouvre une session
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-zoo-pu");
        EntityManager em = emf.createEntityManager();
        System.out.println("Entity manager prêt");
 
        // Commence une transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.println("Début de la transaction");
 
        /*
         * Début du code spécifique
         */
        for(String nom : args) {
            // Création d'un animal
            Animal animal = new Animal(nom, new Date());
            System.out.println(animal.toString() + " créé");
 
            // Rend celui-ci persistent dans la base de données
            em.persist(animal);
        }
        /*
         * Fin du code spécifique
         */
 
        // Applique les modifications à la base de données
        tx.commit();
        System.out.println("Transaction confirmée");
 
        // Ferme la session et termine JPA
        em.close();
        emf.close();
    }
}

Et maintenant notre second programme. Celui-ci effectue une requête JPQL pour récupérer l'ensemble des animaux de la base, et les affiche:

package fr.chicoree.jpa.zoo;
 
import javax.persistence.*;
import java.util.*;
 
public class ListeAnimaux {
    public static void main(String[] args) {
        // Démarre JPA et ouvre une session
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-zoo-pu");
        EntityManager em = emf.createEntityManager();
        System.out.println("Entity manager prêt");
 
        // Commence une transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.println("Début de la transaction");
 
        /*
         * Début du code spécifique
         */
        // Requête JPQL
        Query query = em.createQuery("SELECT a FROM Animal a ORDER BY entrée DESC");
 
        @SuppressWarnings("unchecked")
        List<Animal> animaux = (List<Animal>)query.getResultList();
        for(Animal a : animaux) {
            System.out.println(a);
        }
        /*
         * Fin du code spécifique
         */
 
        // Applique les modifications à la base de données
        tx.commit();
        System.out.println("Transaction confirmée");
 
        // Ferme la session et termine JPA
        em.close();
        emf.close();
    }
}

Avant d'aller plus loin, un rapide examen de ces sources s'impose. En fait, vous remarquerez qu'une grande partie du code de ces deux programmes est identique. Plus précisément, le début du programme, chargé de se connecter à JPA et d'ouvrir une transaction avec la base de données – et la fin du programme qui confirme la transaction et clos la connexion à JPA.

Comme vous le comprenez sûrement, tout le code manipulant des entités persistantes doit prendre place entre le début de la transaction et la confirmation de celle-ci. Bien entendu, un programme peut créer autant de transaction qu'il le désire. Mais nous laisserons cette possibilité de côté ici.

Pour examiner un peu plus le code spécifique à chacun de ces programmes, vous voyez aussi que rendre persistant un objet Java se résume à l'appel de méthode suivant:

em.persist(animal);

Et que la "récupération" d'objets préalablement stockés dans la base se fait par une requête dans un langage spécifique appelé JPQL et qui est assez proche de SQL. Si vous êtes familiers de JDBC, vous devriez être en territoire connu. La grosse différence étant qu'ici on récupère une liste d'objets – et non pas une liste d'enregistrements.

Le fichier de configuration

Une des forces de JPA est son architecture modulaire. Ainsi, il est possible de facilement changer de fournisseur de persistance. Ou de serveur de base de données. Et tout cela sans incidence sur le code source. Comme souvent dans le monde Java, ceci est rendu possible par l'utilisation d'un fichier de configuration XML. Ici, il s'agit du fichier persistence.xml à placer dans le répertoire META-INF (que vous devrez créer à la racine de votre projet). Voici le code de META-INF/persistence.xml:

<persistence>
    <persistence-unit name="jpa-zoo-pu">
        <!-- Définit Hibernate comme fournisseur de persistence -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <properties>
            <property name="hibernate.archive.autodetection" value="class" />
            <!-- <property name="hibernate.show_sql" value="true" /> -->
            <property name="hibernate.format_sql" value="true" />
 
            <!-- Configuration de la  BDD -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost/jpa-zoo" />
            <property name="hibernate.connection.username" value="jpa-user" />
            <property name="hibernate.connection.password" value="SomePassword" />
 
            <!-- Spécifie le 'dialecte' SQL utilisé pour communiquer avec la BDD -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
 
            <!-- Indique a Hibernate de (re-)créer la BDD au lancement de l'application -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
 
    </persistence-unit>
</persistence>

Grosso-modo, ce fichier de configuration définit Hibernate comme fournisseur de persistance JPA, puis configure Hibernate. Vous remarquerez notamment la configuration de l'accès à la base de données (driver, url, nom d'utilisateur et mot de passe).

Notez aussi que le nom de l'unité de persistance définie dans ce fichier correspond au nom utilisé dans le code source pour ouvrir une session avec JPA:

META-INF/persistence.xml:

<persistence>
    <persistence-unit name="jpa-zoo-pu">
...


fr/chicoree/jpa/zoo/AjouteAnimal.java:

        // Démarre JPA et ouvre une session
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-zoo-pu");
        EntityManager em = emf.createEntityManager();

Compilation?

A ce stade, l'arborescence de votre projet doit être la suivante:

sh$ find .
.
./fr
./fr/chicoree
./fr/chicoree/jpa
./fr/chicoree/jpa/zoo
./fr/chicoree/jpa/zoo/ListeAnimaux.java
./fr/chicoree/jpa/zoo/AjouteAnimal.java
./fr/chicoree/jpa/zoo/TestSansJPA.java
./fr/chicoree/jpa/zoo/Animal.java
./META-INF
./META-INF/persistence.xml

Comme vous brûlez de tester tout cela, passons à la compilation:

sh$ javac fr/chicoree/jpa/zoo/AjouteAnimal.java
fr/chicoree/jpa/zoo/AjouteAnimal.java:3: package javax.persistence does not exist
import javax.persistence.*;
^
fr/chicoree/jpa/zoo/AjouteAnimal.java:9: cannot find symbol
symbol  : class EntityManagerFactory
location: class fr.chicoree.jpa.zoo.AjouteAnimal
       EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-zoo-pu");
       ^
fr/chicoree/jpa/zoo/AjouteAnimal.java:9: cannot find symbol
symbol  : variable Persistence
location: class fr.chicoree.jpa.zoo.AjouteAnimal
       EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-zoo-pu");
                                  ^
fr/chicoree/jpa/zoo/AjouteAnimal.java:10: cannot find symbol
symbol  : class EntityManager
location: class fr.chicoree.jpa.zoo.AjouteAnimal
       EntityManager em = emf.createEntityManager();
       ^
fr/chicoree/jpa/zoo/AjouteAnimal.java:14: cannot find symbol
symbol  : class EntityTransaction
location: class fr.chicoree.jpa.zoo.AjouteAnimal
       EntityTransaction tx = em.getTransaction();
       ^
5 errors

Visiblement, quelque-chose ne va pas. Le message d'erreur package javax.persistence does not exist évoque sans aucun doute un paquet absent. Et effectivement: nous n'avons ajouté aucune des bibliothèques (JARs) nécessaire pour utiliser JPA et Hibernate! Et il en faut un certain nombre... C'est ce dont nous allons nous occuper maintenant.

Installer les bibliothèques d'Hibernate

Hibernate dépend de pas mal d'autres bibliothèques. C'est dire que l'installation peut être fastidieuse. Pour palier à ce problème, la distribution d'Hibernate existe en plusieurs parfums. Selon vos besoins, vous téléchargerez une distribution ou l'autre et normalement vous devriez avoir non seulement Hibernate, mais aussi les bibliothèques nécessaires pour l'utiliser.

Ce qui nous intéresse ici, c'est la version compatible JPA. C'est à dire avec le support pour l'EntityManager. Lors de la rédaction de cet article, cela correspondait au fichier hibernate-entitymanager-3.4.0.GA.zip téléchargé à partir du site http://www.hibernate.org.

Lorsque vous décompresserez ce ZIP, vous constaterez qu'il contient à la fois hibernate-entitymanager.jar mais aussi d'autres JARs dans les sous-dossiers lib et lib/test. Ce sont les bibliothèques nécessaires pour utiliser Hibernate avec JPA. Bref, vous aurez aussi besoin de ces bibliothèques:

sh$ mkdir lib # Crée le répertoire de destination à la racine du projet
sh$ unzip /path/to/hibernate-entitymanager-3.4.0.GA.zip
sh$ cp hibernate-entitymanager-3.4.0.GA/hibernate-entitymanager.jar lib
sh$ cp hibernate-entitymanager-3.4.0.GA/lib/*.jar lib
sh$ cp hibernate-entitymanager-3.4.0.GA/lib/test/*.jar lib
sh$ rm -rf hibernate-entitymanager-3.4.0.GA/

Bien sûr, maintenant que vous avez installé les bibliothèques dans le répertoire lib de votre projet, il faut aussi penser à modifier le CLASSPATH pour que la JVM trouve ces bibliothèques. Heureux utilisateurs d'un JDK Sun 6 ou ultérieur, cela se fait simplement:

sh$ export CLASSPATH='.:lib/*'

Pour les autres, vous devrez inclure un à un les différents JAR du dossier lib dans votre CLASSPATH...

Toute dernière chose, le système de journalisation log4j utilisé dans l'installation de Hibernate/JPA que nous venons de faire nécessite un fichier de configuration log4j.properties à situer à la racine du projet:

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

log4j.rootLogger=INFO, stdout

log4j.logger.org.hibernate=WARN

La liste exacte des bibliothèques dans le dossier lib dépendra de la version d'Hibernate que vous utiliserez. Mais à ce détail près, voici ce que vous devrez avoir dans le répertoire de votre projet:

sh$ find .
 .
 ./lib
 ./lib/hibernate-core.jar
 ./lib/jta.jar
 ./lib/log4j.jar
 ./lib/asm-attrs.jar
 ./lib/javassist.jar
 ./lib/slf4j-api.jar
 ./lib/junit.jar
 ./lib/dom4j.jar
 ./lib/cglib.jar
 ./lib/hibernate-annotations.jar
 ./lib/ejb3-persistence.jar
 ./lib/asm.jar
 ./lib/commons-collections.jar
 ./lib/hibernate-commons-annotations.jar
 ./lib/hibernate-entitymanager.jar
 ./lib/antlr.jar
 ./lib/slf4j-log4j12.jar
 ./fr
 ./fr/chicoree
 ./fr/chicoree/jpa
 ./fr/chicoree/jpa/zoo
 ./fr/chicoree/jpa/zoo/ListeAnimaux.java
 ./fr/chicoree/jpa/zoo/AjouteAnimal.java
 ./fr/chicoree/jpa/zoo/TestSansJPA.java
 ./fr/chicoree/jpa/zoo/Animal.java
 ./log4j.properties
 ./META-INF
 ./META-INF/persistence.xml

Tout y est? Il est donc temps de retenter de compiler le programme.

Compilation !

Nouvelle tentative de compilation:

sh$ javac fr/chicoree/jpa/zoo/*.java

Et cette fois c'est un succès! Aucune raison de s'arrêter en si bon chemin. Essayons donc d'ajouter un animal à notre zoo:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Baloo
23:49:33,408 ERROR DriverManagerConnectionProvider:88 - JDBC Driver class not found: com.mysql.jdbc.Driver
[...]

Et oui: qui dit persistance dit base de données. Et nous n'avons ni installé le driver de notre serveur de bases de données. Ni même créé la base, d'ailleurs!

Préparer la base de données

"Préparer" la base de données pour être utilisée par JPA/Hibernate veut dire deux choses. Tout d'abord installer le driver, puis créer la base elle-même.

Installer le driver est chose aisée: il suffit de copier le JAR du driver dans notre dossier lib (sans oublier le cas échéant de modifier le CLASSPATH):

sh$ cp /path/to/mysql-connector-java.jar lib

Quand à créer la base, ça n'est pas non plus très compliqué. En supposant que comme moi vous utilisez MySQL, voici le résumé des opérations:

sh$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2692
Server version: 5.0.32-Debian_7etch8-log Debian etch distribution

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE `jpa-zoo`;
Query OK, 1 row affected (0.22 sec)

mysql> GRANT ALL ON `jpa-zoo`.* TO 'jpa-user'@localhost IDENTIFIED BY "SomePassword";
Query OK, 0 rows affected (0.34 sec)

Comme vous le voyez dans la transcription de ma session sous le client texte de MySQL, j'ai créé une base de donnée nommée jap-zoo et l'utilisateur jpa-user ayant tous les droits sur cette base. Le nom de la base, de l'utilisateur et son mot de passe ne sont pas choisis au hasard. Ils sont identiques à ceux indiqués dans le fichier de configuration de JPA META-INF/persistence.xml:

           <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
           <property name="hibernate.connection.url" value="jdbc:mysql://localhost/jpa-zoo" />
           <property name="hibernate.connection.username" value="jpa-user" />
           <property name="hibernate.connection.password" value="SomePassword" />

Voilà: c'est tout ce qu'il y a à configurer dans la base de données. Vous comprenez aussi que changer de serveur de base de données se résume à quelques modifications dans le fichier persistence.xml. J'attire aussi votre attention sur le fait qu'il n'est pas nécessaire de créer la moindre table! En effet, Hibernate s'en chargera pour nous...

Les entités

Cette fois ça y est? Toutes les bibliothèques sont en place. Les fichiers de configurations prêts. La base de données installée et les sources compilés depuis un bon moment. Rien ne peut plus échouer? Essayons:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Baloo
Entity manager prêt
Début de la transaction
Baloo [2009-05-03] créé
Exception in thread "main" java.lang.IllegalArgumentException: Unknown entity: fr.chicoree.jpa.zoo.Animal
        at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:223)
        at fr.chicoree.jpa.zoo.AjouteAnimal.main(AjouteAnimal.java:27)

C'était bien parti, mais visiblement, le programme a échoué au moment de rendre persistant notre animal. Et le message d'erreur peut être trompeur:

Unknown entity: fr.chicoree.jpa.zoo.Animal

On pourrait penser au premier abord que la classe fr.chicoree.jpa.zoo.Animal n'est pas disponible. Mais l'erreur se produit au moment où on rend persistant l'objet. Il est donc déjà instancié. La classe a bien été trouvée par la JVM, mais Hibernate ne reconnaît pas cette classe comme une entité. C'est à dire comme une classe dont les instances peuvent être sauvegardées et rechargées à partir de la base de données. Nous allons donc voir ce qu'il faut faire pour transformer une classe ordinaire en entité.

Et rassurez-vous: autant la configuration initiale d'Hibernate est lourde, autant définir une entité est simple. En effet, il ne faut guère plus de choses que d'ajouter deux annotations à notre classe initiale.

Ces deux annotations sont dans le package javax.persitence. Il faudra donc également penser à l'importer.

Par ailleurs, JPA impose qu'une entité dispose d'un constructeur sans argument. Il faut donc aussi penser à l'ajouter. Au final, le code complet de la classe fr.chicoree.jpa.zoo.Animal est le suivant:

package fr.chicoree.jpa.zoo;
 
import java.util.Date;
import javax.persistence.*;
 
@Entity
public class Animal {
    // Required by JPA
    private Animal() {}
 
    public Animal(String nom, Date entrée) {
        this.nom = nom;
        this.entrée = entrée;
    }
 
    public String getNom() {
        return nom;
    }
 
    public Date getEntrée() {
        return (Date)entrée.clone();
    }
 
    public String toString() {
        return String.format("%s [%tF]", nom, entrée);
    }
 
    @Id
    private String      nom;
    private Date        entrée;
}

Faites donc les modifications nécessaires, et re-compilons – puis re-testons notre programme:

sh$ javac fr/chicoree/jpa/zoo/Animal.java
sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Baloo
Entity manager prêt
Début de la transaction
Baloo [2009-05-03] créé
Transaction confirmée

Visiblement, le programme arrive au bout de son exécution sans exception. Essayons de voir s'il est possible d'afficher la liste des animaux:

sh$ java fr.chicoree.jpa.zoo.ListeAnimaux
Entity manager prêt
Début de la transaction
Transaction confirmée

Déçu, n'est ce pas? Aurait-on encore oublié un JAR ou un fichier de configuration pour que Hibernate ne fonctionne pas convenablement? Et bien, non. Pas du tout. Et même, dans cet exemple Hibernate fonctionne très bien. Exactement comme on le lui a demandé. Ce qui n'est pas forcément ce que l'on voulait arrivé à ce stade...

Création automatique des tables

Pour comprendre ce qui s'est passé, nous aurons besoin d'aller jeter un oeil dans la base de données, pour voir ce qu'y a fait Hibernate. Pour que nous soyons tous dans le même cas de figure, relancez encore une fois l'ajout d'un animal:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Baloo
Entity manager prêt
Début de la transaction
Baloo [2009-05-03] créé
Transaction confirmée

Et allons voir si notre animal a été enregistré ou pas:

sh$ mysql -u root -p jpa-zoo
password:
[...]
mysql> SHOW TABLES;
+-------------------+
| Tables_in_jpa-zoo |
+-------------------+
| Animal            |
+-------------------+
1 row in set (0.00 sec)

Visiblement, Hibernate a créé une table pour notre entité. Regardons un peu comment elle est structurée:

mysql> EXPLAIN Animal;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| nom    | varchar(255) | NO   | PRI |         |       |
| entrée | datetime     | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+ 
2 rows in set (0.08 sec) 

Comme vous le constatez, lorsqu'Hibernate a créé la table, le champ annoté @Id est devenu la clé primaire. Remarquez aussi qu'Hibernate essaye de faire de son mieux pour faire correspondre le type d'une colonne de la table avec le type du champ. Ainsi, String est devenu varchar. Alors que java.util.Date a été mappé sur une colonne de type datetime.

La table est là. Et sa structure semble raisonnable. Qu'en est-il des données?

mysql> SELECT * FROM Animal;
+-------+---------------------+
| nom   | entrée              |
+-------+---------------------+
| Baloo | 2009-05-03 00:32:38 |
+-------+---------------------+
1 row in set (0.00 sec)

Et bien? Notre ours y est! Y aurait-il un bug dans le programme ListeAnimaux? En fait non. Mais pour vous en convaincre, relançons ce dernier et voyons ce qui arrive à la base de données:

sh$ java fr.chicoree.jpa.zoo.ListeAnimaux
Entity manager prêt
Début de la transaction
Transaction confirmée
mysql> SELECT * FROM Animal;
Empty set (0.00 sec)

Les données ont disparu! Quelle est la cause de ce mystère? Et bien il faut la rechercher dans le fichier de configuration persistence.xml. Au niveau de la propriété hibernate.hbm2ddl.auto:

           <property name="hibernate.hbm2ddl.auto" value="create" />

La valeur create signifie qu'Hibernate doit recréer la base de données à chaque lancement d'une application. C'est pratique quand on vient d'ajouter une entité dans un programme, ou quand les relations entre entités ont été modifiées. Par contre, cela a pour effet de bord de perdre toutes les données stockées dans les tables. Ici, ce n'est pas ce que nous voulons. Désactivons donc cette fonctionnalité:

           <property name="hibernate.hbm2ddl.auto" value="" />

Rien à compiler. A peine le fichier de configuration enregistré, Hibernate adoptera ce nouveau mode de fonctionnement. Vérifions:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Baloo
Entity manager prêt
Début de la transaction
Baloo [2009-05-03] créé
Transaction confirmée
sh$ java fr.chicoree.jpa.zoo.ListeAnimaux
Entity manager prêt
Début de la transaction
Baloo [2009-05-03]
Transaction confirmée

Ca y est enfin: nous avons réussi à faire persister un objet dans la base de données. Puis à le recharger.

Exploration

Pour terminer cet article d'introduction, nous allons explorer quelques unes des facettes liées à la persistance avec Hibernate. Nous verrons tout d'abord comment Hibernate est dépendant des services fournis par le serveur de base de données sous-jascent. Et ensuite que les objets persistants dans la base de données peuvent avoir d'autres origines qu'un programme utilisant JPA ...

Contraintes et transactions

Pour explorer la dépendance entre Hibernate et le serveur de base de données, nous allons commencer par regarder un peu comment se comporte l'application si l'on essaye de rentrer un animal déjà présent dans la base de données:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Bagheera
Entity manager prêt
Début de la transaction
Bagheera [2009-05-03] créé
Transaction confirmée

Pour l'instant, Bagheera n'était pas dans la base de données. L'ajout se passe sans problème. Ce qui peut se vérifier en examinant le contenu de la table Animal:

mysql> SELECT * FROM Animal;
+----------+---------------------+
| nom      | entrée              |
+----------+---------------------+
| Baloo    | 2009-05-03 00:40:21 |
| Bagheera | 2009-05-03 12:17:19 |
+----------+---------------------+
2 rows in set (0.00 sec)

Alors que se passe-t-il maintenant si l'on tente d'ajouter une seconde fois un animal du même nom:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Bagheera
Entity manager prêt
Début de la transaction
Bagheera [2009-05-03] créé
12:20:49,395  WARN JDBCExceptionReporter:100 - SQL Error: 1062, SQLState: 23000
12:20:49,397 ERROR JDBCExceptionReporter:101 - Duplicate entry 'Bagheera' for key 1
12:20:49,399 ERROR AbstractFlushingEventListener:324 - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
[...]

Comme vous le voyez, il ne nous est pas possible d'entrer deux animaux de même nom. Ce qui se comprend facilement, puisque le nom sert de clé primaire. Donc rien d'extraordinaire ici. Et surtout rien qui ne nous renseigne sur le fonctionnement d'Hibernate.

Mais que se passerait-il si nous essayons de rajouter plusieurs animaux, dont l'un est déjà présent dans la base?

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Nag Nagaina Bagheera
Entity manager prêt
Début de la transaction
Nag [2009-05-03] créé
Nagaina [2009-05-03] créé
Bagheera [2009-05-03] créé
12:28:40,807  WARN JDBCExceptionReporter:100 - SQL Error: 1062, SQLState: 23000
12:28:40,809 ERROR JDBCExceptionReporter:101 - Duplicate entry 'Bagheera' for key 1
12:28:40,811 ERROR AbstractFlushingEventListener:324 - Could not synchronize database state with session

Bien entendu, l'ajout échoue sur la clé dupliquée. Mais qu'est-il arrivé à Nag et Nagaina, les deux autres animaux ajoutés? Ont-ils été enregistrés dans la base de données ou pas? Si vous êtes un "pro" des bases de données vous devez déjà vous dire que puisque nous étions dans une transaction, celle-ci doit s'exécuter de manière atomique: soit les 3 animaux ont étés ajoutés. Soit aucun. Et là, très visiblement nous devrions être dans le second cas. Allons vérifier:

mysql> SELECT * FROM Animal;
+----------+---------------------+
| nom      | entrée              |
+----------+---------------------+
| Baloo    | 2009-05-03 00:40:21 |
| Bagheera | 2009-05-03 12:17:19 |
| Nag      | 2009-05-03 12:28:40 |
| Nagaina  | 2009-05-03 12:28:40 |
+----------+---------------------+
4 rows in set (0.00 sec)

Surpris? Et oui: les premiers animaux ont été enregistrés! Mais alors, à quoi sert la transaction JPA? Y-aurait-il un bug dans Hibernate?

En fait non: la limitation que nous observons ici n'est pas dû à JPA ou Hibernate. Mais au serveur de base de données que nous utilisons. En effet, les garanties que peut assurer JPA/Hibernate sont liées aux capacités de la base de données sous-jacente. Or, MySQL n'est pas transactionnel. Au moins dans le cas de tables MyISAM. Et c'est le type de table qu'a créé pour nous Hibernate:

mysql> SHOW CREATE TABLE Animal\G
*************************** 1. row ***************************
       Table: Animal
Create Table: CREATE TABLE `Animal` (
  `nom` varchar(255) NOT NULL,
  `entrée` datetime default NULL,
  PRIMARY KEY  (`nom`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

Dans MySQL, la solution à ce problème est d'utiliser des tables InnoDB plutôt que des tables MyISAM. Ce qu'Hibernate sait faire, mais à condition de lui préciser d'utiliser le bon dialecte SQL. Souvenez-vous, de ce que nous avions dans le fichier persistence.xml:

           <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />

Il faut donc remplacer ce dialecte par celui qui crée des tables InnoDB. Au passage, je demande aussi à Hibernate de recréer les tables au lancement de l'application:

           <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
           <property name="hibernate.hbm2ddl.auto" value="create" />

Et testons le programme:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Bagheera
Entity manager prêt
Début de la transaction
Bagheera [2009-05-03] créé
Transaction confirmée
mysql> SHOW CREATE TABLE Animal\G
*************************** 1. row ***************************
       Table: Animal
Create Table: CREATE TABLE `Animal` (
  `nom` varchar(255) NOT NULL,
  `entrée` datetime default NULL,
  PRIMARY KEY  (`nom`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> SELECT * FROM Animal;
+----------+---------------------+
| nom      | entrée              |
+----------+---------------------+
| Bagheera | 2009-05-03 12:49:53 |
+----------+---------------------+
1 row in set (0.00 sec)

C'est bien une table InnoDB qui a été (re-)créée. Et Bagheera est bien enregistrée dans la table. Pour ne pas oublier, désactivons tout de suite la création automatique des tables:

sh$ sed -i '/hbm2ddl.auto/s/"create"/""/' META-INF/persistence.xml
sh$ grep hbm2ddl.auto META-INF/persistence.xml # Juste pour vérifier...
           <property name="hibernate.hbm2ddl.auto" value="" />

Et retentons d'ajouter trois animaux avec un doublon:

sh$ java fr.chicoree.jpa.zoo.AjouteAnimal Nag Nagaina Bagheera
Entity manager prêt
Début de la transaction
Nag [2009-05-03] créé
Nagaina [2009-05-03] créé
Bagheera [2009-05-03] créé
13:02:29,772  WARN JDBCExceptionReporter:100 - SQL Error: 1062, SQLState: 23000
13:02:29,774 ERROR JDBCExceptionReporter:101 - Duplicate entry 'Bagheera' for key 1
13:02:29,776 ERROR AbstractFlushingEventListener:324 - Could not synchronize database state with session

org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update

Même erreur que tout à l'heure. Mais la différence est dans ce qui s'est passé dans la base de données:

mysql> SELECT * FROM Animal;
+----------+---------------------+
| nom      | entrée              |
+----------+---------------------+
| Bagheera | 2009-05-03 12:49:53 |
+----------+---------------------+
1 row in set (0.00 sec)

Aucune entrée n'a été ajoutée: dans MySQL, les tables InnoDB garantissent l'atomicité des transactions. Puisqu'une insertion a échouée l'intégralité de la transaction est annulée (rollback).

Tout cela pour dire que même si le choix de la base de données semble secondaire puisqu'elles sont facilement interchangeables, les fonctionnalités supportées par Hibernate sont fortement dépendantes des capacités – et de la configuration – du serveur de base de données sous-jacent. Ce choix doit donc être réfléchi en fonction des garanties de fonctionnement souhaitées.

Créer des objets "à la main"

Autre point dont il faut avoir conscience lors de l'utilisation de JPA: Jusqu'à présent nous avons beaucoup employé le mot de "persistance". Comme si nous enregistrions vraiment un objet dans la base de données. Puis le rechargions au besoin. Mais en réalité JPA n'est rien d'autre qu'une passerelle (bridge) entre le modèle objet utilisé par Java et le modèle relationnel utilisé par la base de données. Et en fait, il s'agit plus de copier dans la base les données d'un objet – puis d'être capable ultérieurement d'initialiser un nouvel objet à partir de ces données.

Autrement dit, rien n'oblige les données utilisées pour (re-)créer un objet à provenir de JPA. Faites l'expérience d'ajouter vous-même un enregistrement à la table Animal, puis relancer le programme ListeAnimaux. Il n'y a aucune différence pour JPA entre l'objet rendu persistant, et celui inséré "à la main" dans la base de données:

mysql> INSERT INTO Animal VALUES ('Kaa', NOW());
Query OK, 1 row affected (0.14 sec) 

mysql> SELECT * FROM Animal;
+----------+---------------------+
| nom      | entrée              |
+----------+---------------------+
| Bagheera | 2009-05-03 12:49:53 |
| Kaa      | 2009-05-03 17:13:31 |
+----------+---------------------+
2 rows in set (0.00 sec)
sh$ java fr.chicoree.jpa.zoo.ListeAnimaux Entity manager prêt
Début de la transaction
Kaa [2009-05-03]
Bagheera [2009-05-03]
Transaction confirmée

Cela ouvre des possibilités d'interopérabilité entre applications utilisant JPA et applications utilisant des requêtes directes à la base de données.

Conclusion

Voilà, nous avons fini notre rapide présentation de JPA/Hibernate. Comme vous l'avez constaté, la mise en oeuvre initiale d'Hibernate peut être assez longue. Mais une fois en place, développer avec Hibernate – c'est à dire définir des entités, rendre des objets persistants ou les recharger – devient extrêmement aisé. Et même si cette technologie n'est pas forcément la solution à tous les problèmes, cela vaut largement le coup de consacrer quelques heures à l'étudier!