Ce tutoriel détaille la mise en place d'Hibernate 2.1 avec le conteneur de servlet Apache Tomcat sur une application web. Hibernate est prévu pour fonctionner à la fois dans un environnement managé tel que proposé par tous les plus grands serveurs d'applications J2EE, mais aussi dans les applications Java autonomes. Bien que le système de base de données utilisé dans ce toturiel soit PostgreSQL 7.3, le support d'autres bases de données n'est qu'une question de configuration du dialecte SQL d'Hibernate.
Premièrement, nous devons copier toutes les bibliothèques nécessaires à l'installation dans Tomcat. Utilisant un contexte web séparé (webapps/quickstart) pour ce tutoriel, nous devons faire attention à la fois au chemin vers des bibliothèques globales (TOMCAT/common/lib) et au chemin du classloader contextuel de la webapp dans webapps/quickstart/WEB-INF/lib (pour les fichiers JAR) et webapps/quickstart/WEB-INF/classes. On se réfèrera aux deux niveaux de classloader que sont le classloader de classpath global et de classpath contextuel de la webapp.
Maintenant, copions les bibliothèques dans les deux classpaths :
Copiez le pilote JDBC de la base de données dans le classpath global. C'est nécessaire à l'utilisation du pool de connexions DBCP qui vient avec Tomcat. Hibernate utilise les connexions JDBC pour exécuter les ordres SQL sur la base de données, donc vous devez soit fournir les connexions JDBC poolées, soit configurer Hibernate pour utiliser l'un des pools nativement supportés (C3P0, Proxool). Pour ce tutoriel, copiez la blbliothèque pg73jdbc3.jar (pour PostgreSQL 7.3 et le JDK 1.4) dans le classpath global. Si vous voulez utiliser une base de données différente, copiez simplement le pilote JDBC approprié.
Ne copiez jamais autre chose dans le classpath global de Tomcat ou vous auriez des problèmes avec divers outils tels que log4j, commons-logging, et d'autres. Utilisez toujours le classpath contextuel de la webapp propre à chaque application, et donc copiez les bibliothèques dans WEB-INF/lib, puis copiez vos propres classes ainsi que les fichiers de configuration/de propriété dans WEB-INF/classes. Ces deux répertoires sont, par définition de la spécification J2EE, dans le classpath contextuel de la webapp.
Hibernate se présente sous la forme d'une blbliothèque JAR. Le fichier hibernate2.jar doit être copié dans le classpath contextuel de la webapp avec les autres classes de l'application. Hibernate a besoin de quelques bibliothèques tierces à l'exécution, elles sont embarquées dans la distribution Hibernate et se trouvent dans le répertoire lib/ ; voir Tableau 1.1, « Bibliothèques tierces nécessaires à Hibernate ». Copiez les bibliothèques tierces requises dans le classpath de contexte.
Tableau 1.1. Bibliothèques tierces nécessaires à Hibernate
Bibliothèque | Description |
---|---|
dom4j (requise) | Hibernate utilise dom4j pour lire la configuration XML et les fichiers XML de métadonnées du mapping. |
CGLIB (requise) | Hibernate utilise cette bibliothèque de génération de code pour étendre les classes à l'exécution (en conjonction avec la réflexion Java). |
Commons Collections, Commons Logging (requises) | Hibernate utilise diverses bibliothèques du projet Apache Jakarta Commons. |
ODMG4 (requise) | Hibernate est compatible avec l'interface de gestion de la persistance telle que définie par l'ODMG. Elle est nécessaire si vous voulez mapper des collections même si vous n'avez pas l'intention d'utiliser l'API de l'ODMG. Nous ne mappons pas de collections dans ce tutoriel, mais, quoi qu'il arrive c'est une bonne idée de copier ce JAR. |
EHCache (requise) | Hibernate peut utiliser diverses implémentations de cache de second niveau. EHCache est l'implémentation par défaut (tant qu'elle n'est pas changée dans le fichier de configuration). |
Log4j (optionnelle) | Hibernate utilise l'API Commons Logging, qui peut utiliser log4j comme mécanisme de log sous-jacent. Si la bibliothèque Log4j est disponible dans le classpath, Commons Logging l'utilisera ainsi que son fichier de configuration log4j.properties récupéré depuis le classpath. Un exemple de fichier de propriétés pour log4j est embarqué dans la distribution d'Hibernate. Donc, copiez log4j.jar et le fichier de configuration (qui se trouve dans src/) dans le classpath contextuel de la webapp si vous voulez voir ce que fait Hibernate pour vous. |
Nécessaire ou pas ? | Jetez un coup d'oeil à lib/README.txt de la distribution d'Hibernate. C'est une liste à jour des bibliothèques tierces distribuées avec Hibernate. Vous y trouverez toutes les bibliothèques listées et si elles sont requises ou optionnelles. |
Nous allons maintenant configurer le pool de connexions à la base de données à la fois dans Tomcat mais aussi dans Hibernate. Cela signifie que Tomcat proposera des connexions JDBC poolées (en s'appuyant sur son pool DBCP), et qu'Hibernate demandera ces connexions à travers le JNDI. Tomcat proposant l'accès au pool de connexions via JNDI, nous ajoutons la déclaration de ressource dans le fichier de configuration principal de Tomcat (TOMCAT/conf/server.xml) :
<Context path="/quickstart" docBase="quickstart"> <Resource name="jdbc/quickstart" scope="Shareable" type="javax.sql.DataSource"/> <ResourceParams name="jdbc/quickstart"> <parameter> <name>factory</name> <value>org.apache.commons.dbcp.BasicDataSourceFactory</value> </parameter> <!-- paramètres de connexion DBCP à la base de données --> <parameter> <name>url</name> <value>jdbc:postgresql://localhost/quickstart</value> </parameter> <parameter> <name>driverClassName</name><value>org.postgresql.Driver</value> </parameter> <parameter> <name>username</name> <value>quickstart</value> </parameter> <parameter> <name>password</name> <value>secret</value> </parameter> <!-- options du pool de connexion DBCP --> <parameter> <name>maxWait</name> <value>3000</value> </parameter> <parameter> <name>maxIdle</name> <value>100</value> </parameter> <parameter> <name>maxActive</name> <value>10</value> </parameter> </ResourceParams> </Context>
Le contexte web que l'on a configuré dans cet exemple se nomme quickstart, son répertoire de base étant TOMCAT/webapp/quickstart. Pour accéder aux servlets, appeler l'URL http://localhost:8080/quickstart à partir de votre navigateur (après avoir bien entendu ajouté le nom de votre servlet et l'avoir lié dans votre fichier web.xml). Vous pouvez également commencer à créer une servlet simple qui possède une méthode process() vide.
Tomcat utilise le pool de connexions DBCP avec sa configuration et fournit les Connections JDBC poolées à travers l'interface JNDI à l'adresse java:comp/env/jdbc/quickstart. Si vous éprouvez des problèmes pour faire fonctionner le pool de connexions, référez-vous à la documentation Tomcat. Si vous avez des messages de type exception du pilote JDBC, commencez par configurer le pool de connexions JDBC sans Hibernate. Des tutoriels sur Tomcat et JDBC sont disponibles sur le Web.
La prochaine étape consiste à configurer Hibernate pour utiliser les connexions du pool attaché au JNDI. Nous allons utiliser le fichier de configuration XML d'Hibernate. L'approche basique utilisant le fichier .properties est équivalente fonctionnellement, mais n'offre pas d'avantage. Nous utiliserons le fichier de configuration XML parce que c'est souvent plus pratique. Le fichier de configuration XML est placé dans le classpath contextuel de la webapp (WEB-INF/classes), sous le nom hibernate.cfg.xml :
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd"> <hibernate-configuration> <session-factory> <property name="connection.datasource">java:comp/env/jdbc/quickstart</property> <property name="show_sql">false</property> <property name="dialect">net.sf.hibernate.dialect.PostgreSQLDialect</property> <!-- fichiers de mapping --> <mapping resource="Cat.hbm.xml"/> </session-factory> </hibernate-configuration>
Le fichier de configuration montre que nous avons stoppé la log des commandes SQL, positionné le dialecte SQL de la base de données utilisée, et fournit le lien où récupérer les connexions JDBC (en déclarant l'adresse JNDI à laquelle est attachée le pool de source de données). Le dialecte est un paramètrage nécessaire du fait que les bases de données diffèrent dans leur interprétation du SQL "standard". Hibernate s'occupe de ces différences et vient avec des dialectes pour toutes les bases de données les plus connues commerciales ou open sources.
Une SessionFactory est un concept Hibernate qui représente un et un seul entrepôt de données ; plusieurs bases de données peuvent être utilisées en créant plusieurs fichiers de configuration XML, plusieurs objets Configuration et SessionFactory dans votre application.
Le dernier élément de hibernate.cfg.xml déclare Cat.hbm.xml comme fichier de mapping Hibernate pour la classe Cat. Ce fichier contient les métadonnées du lien entre la classe Java (aussi appelé POJO pour Plain Old Java Object) et une table de la base de données (voire plusieurs tables). Nous reviendrons bientôt sur ce fichier. Commençons par écrire la classe java (ou POJO) et déclarons les métadonnées de mapping pour celle-ci.
Hibernate fonctionne au mieux dans un modèle de programmation consistant à utiliser de Bon Vieux Objets Java (Plain Old Java Objects - POJO) pour les classes persistantes (NdT: on parle de POJO en comparaison d'objets de type EJB ou d'objets nécessitants d'hériter d'une quelconque classe de base). Un POJO est souvent un JavaBean dont les propriétés de la classe sont accessibles via des getters et des setters qui encapsulent la représentation interne dans une interface publique :
package net.sf.hibernate.examples.quickstart; public class Cat { private String id; private String name; private char sex; private float weight; public Cat() { } public String getId() { return id; } private void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public char getSex() { return sex; } public void setSex(char sex) { this.sex = sex; } public float getWeight() { return weight; } public void setWeight(float weight) { this.weight = weight; } }
Hibernate ne restreint pas l'usage des types de propriétés ; tous les types du JDK et les types primitifs (comme String, char et Date) peuvent être mappés, ceci inclus les classes du framework de collection de Java. Vous pouvez les mapper en tant que valeurs, collections de valeurs ou comme associations avec les autres entités. id est une propriété spéciale qui représente l'identifiant dans la base de données pour cette classe (appelé aussi clé primaire). Cet identifiant est chaudement recommandé pour les entités comme Cat : Hibernate peut utiliser les identifiants pour son seul fonctionnement interne (non visible de l'application) mais vous perdriez en flexibilité dans l'architecture de votre application.
Les classes persistantes n'ont besoin d'implémenter aucune interface particulière et n'ont pas besoin d'hériter d'une quelconque classe de base. Hibernate n'utilise également aucun mécanisme de manipulation des classes à la construction, tel que la manipulation du byte-code ; il s'appuie uniquement sur le mécanisme de réflexion de Java et sur l'extension des classes à l'exécution (via CGLIB). On peut donc, sans la moindre dépendance entre les classes POJO et Hibernate, les mapper à une table de la base de données.
Le fichier de mapping Cat.hbm.xml contient les métadonnées requises pour le mapping objet/relationnel. Les métadonnées contiennent la déclaration des classes persistantes et le mapping entre les propriétés (les colonnes, les relations de type clé étrangère vers les autres entités) et les tables de la base de données.
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="net.sf.hibernate.examples.quickstart.Cat" table="CAT"> <!-- Une chaîne de 32 caractères hexadécimaux est notre clé technique. Elle est générée automatiquement par Hibernate en utilisant le pattern UUID. --> <id name="id" type="string" unsaved-value="null" > <column name="CAT_ID" sql-type="char(32)" not-null="true"/> <generator class="uuid.hex"/> </id> <!-- Un chat possède un nom mais qui ne doit pas être trop long. --> <property name="name"> <column name="NAME" length="16" not-null="true"/> </property> <property name="sex"/> <property name="weight"/> </class> </hibernate-mapping>
Toute classe persistante doit avoir un identifiant (en fait, uniquement les classes représentant des entités, pas les valeurs dépendant d'objets, qui sont mappées en tant que composant d'une entité). Cette propriété est utilisée pour distinguer les objets persistants : deux chats sont égaux si l'expression catA.getId().equals(catB.getId()) est vraie, ce concept est appelé identité de base de données. Hibernate fournit en standard un certain nombre de générateurs d'identifiants qui couvrent la plupart des scénarii (notamment les générateurs natifs pour les séquences de base de données, les tables d'identifiants hi/lo, et les identifiants assignés par l'application). Nous utilisons le générateur UUID (recommandé uniquement pour les tests dans la mesure où les clés techniques générées par la base de données doivent être privilégiées). et déclarons que la colonne CAT_ID de la table CAT contient la valeur de l'identifiant généré par Hibernate (en tant que clé primaire de la table).
Toutes les propriétés de Cat sont mappées à la même table. La propriété name est mappée utilisant une déclaration explicite de la colonne de base de données. C'est particulièrement utile dans le cas où le schéma de la base de données est généré automatiquement (en tant qu'ordre SQL - DDL) par l'outil d'Hibernate SchemaExport à partir des déclarations du mapping. Toutes les autres propriétés prennent la valeur par défaut donnée par Hibernate ; ce qui, dans la majorité des cas, est ce que l'on souhaite. La table CAT dans la base de données sera :
Colonne | Type | Modificateurs ---------+-----------------------+--------------- cat_id | character(32) | not null name | character varying(16) | not null sex | character(1) | weight | real | Indexes : cat_pkey primary key btree (cat_id)
Vous devez maintenant créer manuellement cette table dans votre base de données, plus tard, vous pourrez vous référer à Chapitre 15, Guide des outils si vous désirez automatiser cette étape avec l'outil SchemaExport. Cet outil crée un fichier de type DDL SQL qui contient la définition de la table, les contraintes de type des colonnes, les contraintes d'unicité et les index.
Nous sommes maintenant prêts à utiliser la Session Hibernate. C'est l'interface du gestionnaire de persistance, on l'utilise pour sauver et récupérer les Cats respectivement dans et à partir de la base de données. Mais d'abord, nous devons récupérer une Session (l'unité de travail Hibernate) à partir de la SessionFactory :
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Une SessionFactory est responsable d'une base de données et n'accepte qu'un seul fichier de configuration XML (hibernate.cfg.xml). Vous pouver positionner les autres propriétés (voire même changer le méta-modèle du mapping) en utilisant Configuration avant de construire la SessionFactory (elle est immuable). Comment créer la SessionFactory et comment y accéder dans notre application ?
En général, une SessionFactory n'est construite qu'une seule fois, c'est-à-dire au démarrage (avec une servlet de type load-on-startup). Cela veut donc dire que l'on ne doit pas la garder dans une variable d'instance des servlets, mais plutôt ailleurs. Il faut un support de type Singleton pour pouvoir y accéder facilement. L'approche montrée ci-dessous résout les deux problèmes : celui de configuration et celui de la facilité d'accès à SessionFactory.
Nous implémentons HibernateUtil, une classe utilitaire
import net.sf.hibernate.*; import net.sf.hibernate.cfg.*; public class HibernateUtil { private static final SessionFactory sessionFactory; static { try { // Crée la SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (HibernateException ex) { throw new RuntimeException("Problème de configuration : " + ex.getMessage(), ex); } } public static final ThreadLocal session = new ThreadLocal(); public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); // Ouvre une nouvelle Session, si ce Thread n'en a aucune if (s == null) { s = sessionFactory.openSession(); session.set(s); } return s; } public static void closeSession() throws HibernateException { Session s = (Session) session.get(); session.set(null); if (s != null) s.close(); } }
Non seulement cette classe s'occupe de garder SessionFactory dans un de ses attributs statiques, mais en plus elle garde la Session du thread courant dans une variable de type ThreadLocal. Vous devez bien comprendre le concept Java de variable de type tread-local (locale à un thread) avant d'utiliser cette classe utilitaire.
Une SessionFactory est threadsafe : beaucoup de threads peuvent y accéder de manière concurrente et demander une Session. Une Session est un objet non threadsafe qui représente une unité de travail avec la base de données. Les Sessions sont ouvertes par la SessionFactory et sont fermées quand le travail est terminé :
Session session = HibernateUtil.currentSession(); Transaction tx= session.beginTransaction(); Cat princess = new Cat(); princess.setName("Princess"); princess.setSex('F'); princess.setWeight(7.4f); session.save(princess); tx.commit(); HibernateUtil.closeSession();
Dans une Session, chaque opération sur la base de données se fait dans une transaction qui isole les opérations de la base de données (c'est également le cas pour les lectures seules). Nous utilisons l'API Transaction pour s'abstraire de la stratégie transactionnelle utilisée (dans notre cas, les transactions JDBC). Cela permet d'avoir un code portable et déployable sans le moindre changement dans un environnement transactionnel géré par le conteneur - CMT - (JTA est utilisé dans ce cas). Il est à noter que l'exemple ci-dessus ne gère pas les exceptions.
Notez également que vous pouvez appeler HibernateUtil.currentSession(); autant de fois que vous voulez, cette méthode vous ramènera toujours la Session courante pour ce thread. Vous devez vous assurer que la Session est fermée après la fin de votre unité de travail et avant que la réponse HTTP soit envoyée. Cela peut être par exemple dans le code de votre servlet ou dans un filtre de servlet. L'effet de bord intéressant de la seconde solution est l'initialisation tardive : la Session est encore ouverte lorsque la vue est construite. Hibernate peut donc charger, lors de votre navigation dans le graphe, les objets qui n'étaient pas initialisés.
Hibernate possède différentes méthodes de récupération des objets à partir de la base de données. La plus flexible est d'utiliser le langage de requêtage d'Hibernate (HQL comme Hibernate Query Language). Ce langage puissant et facile à comprendre est une extension orientée objet du SQL:
Transaction tx = session.beginTransaction(); Query query = session.createQuery("select c from Cat as c where c.sex = :sex"); query.setCharacter("sex", 'F'); for (Iterator it = query.iterate(); it.hasNext();) { Cat cat = (Cat) it.next(); out.println("Chat femelle : " + cat.getName() ); } tx.commit();
Hibernate offre également une API orientée objet de requêtage par critères qui peut être utilisée pour formuler des requêtes typées. Bien sûr, Hibernate utilise des PreparedStatements et les paramètres associés pour toutes ses communications SQL avec la base de données. Vous pouvez également utiliser la fonctionalité de requêtage SQL natif d'Hibernate ou, dans de rares occasions, récupérer un connexion JDBC à partir de la Session.
Nous n'avons fait que gratter la surface d'Hibernate dans ce petit tutoriel. Notez que nous n'avons pas inclus de code spécifique aux servlets dans notre exemple. Vous devez créer vous-même une servlet et y insérer le code Hibernate qui convient.
Garder à l'esprit qu'Hibernate, en tant que couche d'accès aux données, est fortement intégré à votre application. En général, toutes les autres couches dépendent du mécanisme de persistance quel qu'il soit. Soyez sûr de comprendre les implications de ce design.