Ce chapitre est un didacticiel introductif destiné aux nouveaux utilisateurs d'Hibernate. Nous commençons avec une simple application en ligne de commande utilisant une base de données en mémoire, et la développons en étapes faciles à comprendre.
Ce didacticiel est destiné aux nouveaux utilisateurs d'Hibernate mais requiert des connaissances Java et SQL. Il est basé sur un didacticiel de Michael Gloegl, les bibliothèques tierces que nous nommons sont pour les JDK 1.4 et 5.0. Vous pourriez avoir besoin d'autres bibliothèques pour le JDK 1.3.
Le code source de ce tutoriel est inclus dans la distribution dans le répertoire doc/reference/tutorial/.
D'abord, nous créerons une simple application Hibernate en console. Nous utilisons une base de données en mémoire (HSQL DB), donc nous n'avons pas à installer de serveur de base de données.
Supposons que nous ayons besoin d'une petite application de base de données qui puisse stocker des événements que nous voulons suivre, et des informations à propos des hôtes de ces événements.
La première chose que nous faisons est de configurer notre répertoire de développement et de mettre toutes les bibliothèques dont nous avons besoin dedans. Téléchargez la distribution Hibernate à partir du site web d'Hibernate. Extrayez le paquet et placez toutes les bibliothèques requises trouvées dans /lib dans le répertoire /lib de votre nouveau répertoire de travail. Il devrait ressembler à ça :
. +lib antlr.jar cglib-full.jar asm.jar asm-attrs.jars commons-collections.jar commons-logging.jar ehcache.jar hibernate3.jar jta.jar dom4j.jar log4j.jar
Ceci est l'ensemble minimum de bibliothèques requises (notez que nous avons aussi copié hibernate3.jar, l'archive principale) pour Hibernate. Lisez le fichier README.txt dans le répertoire lib/ de la distribution Hibernate pour plus d'informations à propos des biliothèques tierces requises et optionnelles. (En fait, log4j n'est pas requis mais préféré par beaucoup de développeurs.)
Ensuite, nous créons une classe qui réprésente l'événement que nous voulons stocker dans notre base de données.
Notre première classe persistante est une simple classe JavaBean avec quelques propriétés :
package events; 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 et bibliothèques tierces> +src +events Event.java
Dans la prochaine étape, nous informons Hibernate de cette classe persistante.
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="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="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> </class> </hibernate-mapping>
L'élément id est la déclaration de la propriété de l'identifiant, name="id" déclare le nom de la propriété Java - Hibernate utilisera les méthodes getter et setter pour accéder à la propriété. L'attribut column indique à Hibernate quelle colonne de la table EVENTS nous utilisons pour cette clef primaire. L'élément generator imbriqué spécifie la stratégie de génération de l'identifiant, dans ce cas nous avons utilisé increment, laquelle est une méthode très simple utile surtout pour les tests (et didacticiels). Hibernate supporte aussi les identifiants générés par les bases de données, globalement uniques, ainsi que les identifiants assignés par l'application (ou n'importe quelle stratégie que vous avez écrit en extension).
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="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </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 et bibliothèques tierces> +src Event.java Event.hbm.xml
Nous poursuivons avec la configuration principale 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 données. 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.
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
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 { public static final SessionFactory sessionFactory; static { try { // Création de la SessionFactory à partir de 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 final ThreadLocal session = new ThreadLocal(); 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.
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, l'unité de travail courante est liée au thread Java courant qui exécute notre application. Cependant, ce n'est pas tout, vous devez aussi considérer le scope, quand une unité de travail commence et quand elle finit.
Une Session commence lorsqu'elle est vraiment utilisée la première fois, lorsque nous appelons getCurrentSession() pour la première fois. Ensuite, elle est attachée par Hibernate au thread courant. Lorsque la transaction s'achève, par commit ou par rollback, Hibernate détache automatiquement la Session du thread et la ferme pour vous. Si vous invoquez getCurrentSession() une nouvelle 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, puisqu'il permet un découpage flexible de votre code (le code délimitant les transactions peut être séparé du code accédant aux données, nous verrons cela plus loin dans ce tutorial).
A propos du scope de l'unité de travail, la Session Hibernate devrait-elle être utilisée pour exécuter une ou plusieurs opérations en base de données ? L'exemple ci-dessus utilise une Session pour une opération. C'est une pure coïncidence, l'exemple est n'est seulement pas assez complexe pour montrer d'autres approches. Le scope d'une Session Hibernate est flexible mais vous ne devriez jamais concevoir votre application de manière à utiliser une nouvelle Session Hibernate pour chaque opération en base de données. Donc même si vous le voyez quelques fois dans les exemples (très simplistes) suivants, considérez une session par operation comme un anti-pattern. Une véritable application (web) est montrée plus loin dans ce 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.
Maintenant désactivez hbm2ddl en commentant la propriété dans votre fichier hibernate.cfg.xml. Généralement vous la laissez seulement activée dans des tests unitaires en continu, mais une autre exécution de hbm2ddl effacerait tout ce que vous avez stocké - le paramètre de configuration create se traduit en fait par "supprimer toutes les tables du schéma, puis re-créer toutes les tables, lorsque la SessionFactory est construite".
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.
Nous avons mappé une classe d'une entité persistante vers une table. Partons de là et ajoutons quelques associations de classe. D'abord nous ajouterons des gens à notre application, et stockerons une liste d'événements auxquels ils participent.
La première version de la classe Person est simple :
package events; public class Person { private Long id; private int age; private String firstname; private String lastname; public Person() {} // Accessor methods for all properties, private setter for 'id' }
Créez un nouveau fichier de mapping appelé Person.hbm.xml (n'oubliez pas la référence à la DTD)
<hibernate-mapping> <class name="events.Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="native"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> </class> </hibernate-mapping>
Finalement, ajoutez la nouveau mapping à la configuration d'Hibernate :
<mapping resource="events/Event.hbm.xml"/> <mapping resource="events/Person.hbm.xml"/>
Nous allons maintenant créer une association entre ces deux entités. Évidemment, des personnes peuvent participer aux événements, et des événements ont des participants. Les questions de conception que nous devons traiter sont : direction, cardinalité et comportement de la collection.
Nous allons ajouter une collection d'événements à la classe Person. De cette manière nous pouvons facilement naviguer dans les événements d'une personne particulière, sans exécuter une requête explicite - en appelant aPerson.getEvents(). Nous utilisons une collection Java, un Set, parce que la collection ne contiendra pas d'éléments dupliqués et l'ordre ne nous importe pas.
Nous avons besoin d'une association unidirectionnelle, pluri-valuée, implémentée avec un Set. Écrivons le code pour ça dans les classes Java et mappons les :
public class Person { private Set events = new HashSet(); public Set getEvents() { return events; } public void setEvents(Set events) { this.events = events; } }
D'abord nous mappons cette association, mais pensez à l'autre côté. Clairement, nous pouvons la laisser unidirectionnelle. Ou alors, nous pourrions créer une autre collection sur Event, si nous voulons être capable de la parcourir de manière bidirectionnelle, c'est-à-dire avoir anEvent.getParticipants(). Ce n'est pas nécessaire d'un point de vue fonctionnel. Vous pourrez toujours exécuter une requête explicite pour récupérer les participants d'un "event" particulier. Ce choix de conception vous est laissé, mais ce qui reste certains est la cardinalité de l'association: "plusieurs" des deux côtés, nous appelons cela une association many-to-many. Par conséquent nous utilisons un mapping Hibernate many-to-many:
<class name="events.Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="native"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> <set name="events" table="PERSON_EVENT"> <key column="PERSON_ID"/> <many-to-many column="EVENT_ID" class="events.Event"/> </set> </class>
Hibernate supporte toutes sortes de mapping de collection, un <set> étant le plus commun. Pour une association many-to-many (ou une relation d'entité n:m), une table d'association est requise. Chaque ligne dans cette table représente un lien entre une personne et un événement. Le nom de la table est configuré avec l'attribut table de l'élément set. Le nom de la colonne identifiant dans l'association, du côté de la personne, est défini avec l'élément <key>, et le nom de la colonne pour l'événement dans l'attribut column de <many-to-many>. Vous devez aussi donner à Hibernate la classe des objets de votre collection (c'est-à-dire : la classe de l'autre côté de la collection).
Le schéma de base de données pour ce mapping est donc :
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | |_____________| |__________________| | PERSON | | | | | |_____________| | *EVENT_ID | <--> | *EVENT_ID | | | | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | |_____________| | FIRSTNAME | | LASTNAME | |_____________|
Réunissons quelques personnes et quelques événements dans une nouvelle méthode dans EventManager :
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); aPerson.getEvents().add(anEvent); session.getTransaction().commit(); }
Après le chargement d'une Person et d'un Event, modifiez simplement la collection en utilisant les méthodes normales de la collection. Comme vous pouvez le voir, il n'y a pas d'appel explicite à update() ou save(), Hibernate détecte automatiquement que la collection a été modifiée et a besoin d'être mise à jour. Ceci est appelé la vérification sale automatique (NdT : "automatic dirty checking"), et vous pouvez aussi l'essayer en modifiant le nom ou la propriété date de n'importe lequel de vos objets. Tant qu'ils sont dans un état persistant, c'est-à-dire, liés à une Session Hibernate particulière (c-à-d qu'ils ont juste été chargés ou sauvegardés dans une unité de travail), Hibernate surveille les changements et exécute le SQL correspondant. Le processus de synchronisation de l'état de la mémoire avec la base de données, généralement seulement à la fin d'une unité de travail, est appelé flushing. Dans notre code, l'unité de travail s'achève par un commit (ou rollback) de la transaction avec la base de données - comme défini par notre option thread de configuration pour la classe CurrentSessionContext.
Vous pourriez bien sûr charger une personne et un événement dans différentes unités de travail. Ou vous modifiez un objet à l'extérieur d'une Session, s'il n'est pas dans un état persistant (s'il était persistant avant, nous appelons cet état détaché). Vous pouvez même modifier une collection lorsqu'elle est détachée:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); Person aPerson = (Person) session .createQuery("select p from Person p left join fetch p.events where p.id = :pid") .setParameter("pid", personId) .uniqueResult(); // Eager fetch the collection so we can use it detached Event anEvent = (Event) session.load(Event.class, eventId); session.getTransaction().commit(); // End of first unit of work aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached // Begin second unit of work Session session2 = HibernateUtil.getSessionFactory().getCurrentSession(); session2.beginTransaction(); session2.update(aPerson); // Reattachment of aPerson session2.getTransaction().commit(); }
L'appel à update rend un objet détaché à nouveau persistant, vous pourriez dire qu'il le lie à une unité de travail, ainsi toutes les modifications (ajout, suppression) que vous avez faites pendant qu'il était détaché peuvent être sauvegardées dans la base de données (il se peut que vous ayez besoin de modifier quelques unes des méthodes précédentes pour retourner cet identifiant).
Cela n'a pas grand intérêt dans notre situation, mais c'est un concept important qu'il vous faut concevoir dans votre application. Pour le moment, complétez cet excercice en ajoutant une nouvelle action à la méthode principale de l'EventManager et invoquez la depuis la ligne de commande. Si vous avez besoin des identifiants d'un client et d'un évènement - la méthode save() vous les retourne (vous devrez peut être modifier certaines méthodes précédentes pour retourner ces identifiants):
else if (args[0].equals("addpersontoevent")) { Long eventId = mgr.createAndStoreEvent("My Event", new Date()); Long personId = mgr.createAndStorePerson("Foo", "Bar"); mgr.addPersonToEvent(personId, eventId); System.out.println("Added person " + personId + " to event " + eventId);
C'était un exemple d'une association entre deux classes de même importance, deux entités. Comme mentionné plus tôt, il y a d'autres classes et d'autres types dans un modèle typique, généralement "moins importants". Vous en avez déjà vu certains, comme un int ou une String. Nous appelons ces classes des types de valeur, et leurs instances dépendent d'une entité particulière. Des instances de ces types n'ont pas leur propre identité, elles ne sont pas non plus partagées entre des entités (deux personnes ne référencent pas le même objet firstname, même si elles ont le même prénom). Bien sûr, des types de valeur ne peuvent pas seulement être trouvés dans le JDK (en fait, dans une application Hibernate toutes les classes du JDK sont considérées comme des types de valeur), vous pouvez aussi écrire vous-même des classes dépendantes, Address ou MonetaryAmount, par exemple.
Vous pouvez aussi concevoir une collection de types de valeur. C'est conceptuellement très différent d'une collection de références vers d'autres entités, mais très ressemblant en Java.
Nous ajoutons une collection d'objets de type de valeur à l'entité Person. Nous voulons stocker des adresses email, donc le type que nous utilisons est String, et la collection est encore un Set :
private Set emailAddresses = new HashSet(); public Set getEmailAddresses() { return emailAddresses; } public void setEmailAddresses(Set emailAddresses) { this.emailAddresses = emailAddresses; }
Le mapping de ce Set :
<set name="emailAddresses" table="PERSON_EMAIL_ADDR"> <key column="PERSON_ID"/> <element type="string" column="EMAIL_ADDR"/> </set>
La différence comparée au mapping vu plus tôt est la partie element, laquelle dit à Hibernate que la collection ne contient pas de références vers une autre entité, mais une collection d'éléments de type String (le nom en minuscule vous indique que c'est un type/convertisseur du mapping Hibernate). Une fois encore, l'attribut table de l'élément set détermine le nom de la table pour la collection. L'élément key définit le nom de la colonne de la clef étrangère dans la table de la collection. L'attribut column dans l'élément element définit le nom de la colonne où les valeurs de String seront réellement stockées.
Regardons le schéma mis à jour :
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | ___________________ |_____________| |__________________| | PERSON | | | | | | | |_____________| | PERSON_EMAIL_ADDR | | *EVENT_ID | <--> | *EVENT_ID | | | |___________________| | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | | *EMAIL_ADDR | |_____________| | FIRSTNAME | |___________________| | LASTNAME | |_____________|
Vous pouvez voir que la clef primaire de la table de la collection est en fait une clef composée, utilisant deux colonnes. Ceci implique aussi qu'il ne peut pas y avoir d'adresses email dupliquées par personne, ce qui est exactement la sémantique dont nous avons besoin pour un ensemble en Java.
Vous pouvez maintenant tester et ajouter des éléments à cette collection, juste comme nous l'avons fait avant en liant des personnes et des événements. C'est le même code en Java.
private void addEmailToPerson(Long personId, String emailAddress) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); // The getEmailAddresses() might trigger a lazy load of the collection aPerson.getEmailAddresses().add(emailAddress); session.getTransaction().commit(); }
Cette fois ci, nous n'avons pas utilisé une requête de chargement agressif (fetch) pour initialiser la collection. Par conséquent, l'invocation du getter déclenchera un select supplémentaire pour l'initialiser. Traquez les logs SQL et tentez d'optimiser ce cas avec un chargement aggressif.
Ensuite nous allons mapper une association bidirectionnelle - faire fonctionner l'association entre une personne et un événement à partir des deux côtés en Java. Bien sûr, le schéma de la base de données ne change pas, nous avons toujours une pluralité many-to-many. Une base de données relationnelle est plus flexible qu'un langage de programmation réseau, donc elle n'a pas besoin de direction de navigation - les données peuvent être vues et récupérées de toutes les manières possibles.
D'abord, ajouter une collection de participants à la classe Event :
private Set participants = new HashSet(); public Set getParticipants() { return participants; } public void setParticipants(Set participants) { this.participants = participants; }
Maintenant mapper ce côté de l'association aussi, dans Event.hbm.xml.
<set name="participants" table="PERSON_EVENT" inverse="true"> <key column="EVENT_ID"/> <many-to-many column="PERSON_ID" class="events.Person"/> </set>
Comme vous le voyez, ce sont des mappings de sets normaux dans les deux documents de mapping. Notez que les noms de colonne dans key et many-to-many sont inversés dans les 2 documents de mapping. L'ajout le plus important ici est l'attribut inverse="true" dans l'élément set du mapping de la collection des Events.
Ce que signifie qu'Hibernate devrait prendre l'autre côté - la classe Person - s'il a besoin de renseigner des informations à propos du lien entre les deux. Ce sera beaucoup plus facile à comprendre une fois que vous verrez comment le lien bidirectionnel entre les deux entités est créé.
Premièrement, gardez à l'esprit qu'Hibernate n'affecte pas la sémantique normale de Java. Comment avons-nous créé un lien entre une Person et un Event dans l'exemple unidirectionnel ? Nous avons ajouté une instance de Event à la collection des références d'événement d'une instance de Person. Donc, évidemment, si vous voulons rendre ce lien bidirectionnel, nous devons faire la même chose de l'autre côté - ajouter une référence de Person à la collection d'un Event. Cette "configuration du lien des deux côtés" est absolument nécessaire et vous ne devriez jamais oublier de le faire.
Beaucoup de développeurs programment de manière défensive et créent des méthodes de gestion de lien pour affecter correctement les deux côtés, par exemple dans Person :
protected Set getEvents() { return events; } protected void setEvents(Set events) { this.events = events; } public void addToEvent(Event event) { this.getEvents().add(event); event.getParticipants().add(this); } public void removeFromEvent(Event event) { this.getEvents().remove(event); event.getParticipants().remove(this); }
Notez que les méthodes get et set pour la collection sont maintenant protégées - ceci permet à des classes du même paquet et aux sous-classes d'accéder encore aux méthodes, mais empêche n'importe qui d'autre de mettre le désordre directement dans les collections (enfin, presque). Vous devriez probablement faire de même avec la collection de l'autre côté.
Et à propos de l'attribut de mapping inverse ? Pour vous, et pour Java, un lien bidirectionnel est simplement une manière de configurer correctement les références des deux côtés. Hibernate n'a cependant pas assez d'informations pour ordonner correctement les expressions SQL INSERT et UPDATE (pour éviter les violations de contrainte), et a besoin d'aide pour gérer proprement les associations bidirectionnelles. Rendre inverse un côté d'une assocation dit à Hibernate de l'ignorer essentiellement, pour le considérer comme un miroir de l'autre côté. C'est tout ce qui est nécessaire à Hibernate pour découvrir tout des problèmes de transformation d'un modèle de navigation directionnelle vers un schéma SQL de base de données. Les règles dont vous devez vous souvenir sont : toutes les associations bidirectionnelles ont besoin d'un côté marqué inverse. Dans une association un-vers-plusieurs vous pouvez choisir n'importe quel côté, il n'y a pas de différence.
Une application web Hibernate utilise la Session et Transaction comme une application standalone. Cependant, quelques patterns sont utiles. Nous allons coder une EventManagerServlet. Cette servlet peut lister tous les évènements stockés dans la base de données, et fournir une formulaire HTML pour saisir d'autres évènements.
Créons une nouvelle classe dans notre répertoire source, dans le package events :
package events; // Imports public class EventManagerServlet extends HttpServlet { // Servlet code }
La servlet n'accepte que les requêtes HTTP GET, la méthode à implémenter est donc doGet() :
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy"); try { // Début de l'unité de travail HibernateUtil.getSessionFactory() .getCurrentSession().beginTransaction(); // Traitement de la requête et rendu de la page... // Fin de l'unité de travail HibernateUtil.getSessionFactory() .getCurrentSession().getTransaction().commit(); } catch (Exception ex) { HibernateUtil.getSessionFactory() .getCurrentSession().getTransaction().rollback(); throw new ServletException(ex); } }
Le pattern que nous utilisons ici est appelé session-per-request. Lorsqu'une requête appelle la servlet, une nouvelle Session Hibernate est ouverte à l'invocation de getCurrentSession() sur la SessionFactory. Ensuite, une transaction avec la base de données est démarrée — tous les accès à la base de données interviennent au sein de la transaction, peu importe que les données soient lues ou écrites (nous n'utilisons pas le mode auto-commit dans les applications).
N'utilisez pas une nouvelle Session Hibernate pour chaque opération en base de données. Utilisez une Session Hibernate qui porte sur l'ensemble de la requête. Utlisez getCurrentSession(), ainsi elle est automatiquement attachée au thread Java courant.
Ensuite, les actions possibles de la requêtes sont exécutées et la réponse HTML est rendue. Nous en parlerons plus tard.
Enfin, l'unité de travail s'achève lorsque l'exécution et le rendu sont achevés. Si un problème survient lors de ces deux phases, une exception est soulevée et la transaction avec la base de données subit un rollback. Voila pour le pattern session-per-request. Au lieu d'avoir un code de délimitant les transactions au sein de chaque servlet, vous pouvez écrire un filtre de servlet. Voir le site Hibernate et le Wiki pour plus d'information sur ce pattern, appelé Open Session in View — vous en aurez besoin dès que vous utiliserez des JSPs et non plus des servlets pour le rendu de vos vues.
Implémentons l'exécution de la requête et le rendu de la page.
// Write HTML header PrintWriter out = response.getWriter(); out.println("<html><head><title>Event Manager</title></head><body>"); // Handle actions if ( "store".equals(request.getParameter("action")) ) { String eventTitle = request.getParameter("eventTitle"); String eventDate = request.getParameter("eventDate"); if ( "".equals(eventTitle) || "".equals(eventDate) ) { out.println("<b><i>Please enter event title and date.</i></b>"); } else { createAndStoreEvent(eventTitle, dateFormatter.parse(eventDate)); out.println("<b><i>Added event.</i></b>"); } } // Print page printEventForm(out); listEvents(out); // Write HTML footer out.println("</body></html>"); out.flush(); out.close();
Ce style de code avec un mix de Java et d'HTML ne serait pas scalable dans une application plus complexe—gardez à l'esprit que nous ne faisons qu'illustrer les concepts basiques d'Hibernate dans ce tutoriel. Ce code affiche une en tête et un pied de page HTML. Dans cette page, sont affichés un formulaire pour la saisie d'évènements ainsi qu'une liste de tous les évènements de la base de données. La première méthode est triviale est ne fait que sortir de l'HTML:
private void printEventForm(PrintWriter out) { out.println("<h2>Add new event:</h2>"); out.println("<form>"); out.println("Title: <input name='eventTitle' length='50'/><br/>"); out.println("Date (e.g. 24.12.2009): <input name='eventDate' length='10'/><br/>"); out.println("<input type='submit' name='action' value='store'/>"); out.println("</form>"); }
La méthode listEvents() utilise la Session Hibernate liée au thread courant pour exécuter la requête:
private void listEvents(PrintWriter out, SimpleDateFormat dateFormatter) { List result = HibernateUtil.getSessionFactory() .getCurrentSession().createCriteria(Event.class).list(); if (result.size() > 0) { out.println("<h2>Events in database:</h2>"); out.println("<table border='1'>"); out.println("<tr>"); out.println("<th>Event title</th>"); out.println("<th>Event date</th>"); out.println("</tr>"); for (Iterator it = result.iterator(); it.hasNext();) { Event event = (Event) it.next(); out.println("<tr>"); out.println("<td>" + event.getTitle() + "</td>"); out.println("<td>" + dateFormatter.format(event.getDate()) + "</td>"); out.println("</tr>"); } out.println("</table>"); } }
FEnfin, l'action store renvoie à la méthode createAndStoreEvent(), qui utilise aussi la Session du thread courant:
protected void createAndStoreEvent(String title, Date theDate) { Event theEvent = new Event(); theEvent.setTitle(title); theEvent.setDate(theDate); HibernateUtil.getSessionFactory() .getCurrentSession().save(theEvent); }
La servlet est faite. Une requête à la servlet sera exécutée par une seule Session et Transaction. Comme pour une application standalone, Hibernate peut automatiquement lier ces objets au thread courant d'exécution. Cela vous laisse la liberté de séparer votre code en couches et d'accéder à la SessionFactory par le moyen que vous voulez. Généralement, vous utiliserez des conceptions plus sophistiquées et déplacerez le code d'accès aux données dans une couche DAO. Voir le wiki Hibernate pour plus d'exemples.
Pour déployer cette application, vous devez créer une archive Web, un War. Ajoutez la cible Ant suivante dans votre build.xml:
<target name="war" depends="compile"> <war destfile="hibernate-tutorial.war" webxml="web.xml"> <lib dir="${librarydir}"> <exclude name="jsdk*.jar"/> </lib> <classes dir="${targetdir}"/> </war> </target>
Cette cible créé un fichier nommé hibernate-tutorial.war dans le répertoire de votre projet. Elle package les bibliothèques et le descripteur web.xml qui est attendu dans le répertoire racine de votre projet:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <servlet> <servlet-name>Event Manager</servlet-name> <servlet-class>events.EventManagerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Event Manager</servlet-name> <url-pattern>/eventmanager</url-pattern> </servlet-mapping> </web-app>
Avant de compiler et déployer l'application web, notez qu'une bibliothèque supplémentaire est requise: jsdk.jar. C'est le kit de développement de Servlet Java, si vous ne disposez pas de cette bibliothèque, prenez la sur le site de Sun et copiez la dans votre répertoire des bibliothèques. Cependant, elle ne sera utilisée uniquement pour la compilation et sera exclue du paackage WAR.
Pour construire et déployer, appelez ant war dans votre projet et copier le fichier hibernate-tutorial.war dans le répertoire webapp de tomcat Si vous n'avez pas installé Tomcat, téléchargez le et suivez la notice d'installation. Vous n'avez pas à modifier la configuration Tomcat pour déployer cette application.
Une fois l'application déployée et Tomcat lancé, accédez à l'application via http://localhost:8080/hibernate-tutorial/eventmanager. Assurez vous de consulter les traces tomcat pour observer l'initialisation d'Hibernate à la première requête touchant votre servlet (l'initialisation statique dans HibernateUtil est invoquée) et pour vérifier qu'aucune exception ne survienne.
Ce didacticiel a couvert les bases de l'écriture d'une simple application Hibernate ainsi qu'une petite application web.
Si vous êtes déjà confiants avec Hibernate, continuez à parcourir les sujets que vous trouvez intéressants à travers la table des matières de la documentation de référence - les plus demandés sont le traitement transactionnel (Chapitre 11, Transactions et accès concurrents), la performance des récupérations d'information (Chapitre 19, Améliorer les performances), ou l'utilisation de l'API (Chapitre 10, Travailler avec des objets) et les fonctionnalités des requêtes (Section 10.4, « Requêtage »).
N'oubliez pas de vérifier le site web d'Hibernate pour d'autres didacticiels (plus spécialisés).