[email protected] Java Persistence Api Cet article décrit une application qui illustre comment la sauvegarde d’un graphe d’objets peut être réalisé en Java Standard Edition via Java Persistence Api. 1. Introduction Java Standard Edition intègre à présent une API de persistance : Java Persistence Api (JPA). JPA permet de réaliser un Mapping Objet Relationnel en assurant la synchronisation d’un graphe d’objets avec des tables dans une base de données. Le but étant de masquer le plus possible la base de données sous-jacente. Il est par exemple possible de générer automatiquement les tables de la base à partir du code Java. De plus, le code, bien qu’à la norme Java Standard Edition, est compatible avec la norme Enterprise Java Bean (EJB 3.0). Technologies utilisées : - Eclipse Hibernate 3 Spring 3 2. Un premier exemple Commençons par étudier un exemple d’utilisation de JPA : EntityManagerFactory emf = … EntityManager em = emf.createEntityManager(); // 4 EntityTransaction tx = em.getTransaction(); tx.begin(); // 3 Voiture // 1 voiture = new Voiture(); em.persist(voiture); // 2 tx.commit(); La partie essentielle sont les lignes 1 et 2 où on créé un objet de type Voiture, mais ce pourrait être n’importe quel objet Java, puis on demande sa sauvegarde dans la basse de données via la méthode em.persist(voiture). Le reste du code, notamment la ligne 3, montre qu’il est possible de réaliser des transactions. L’EntityManager (ligne 4) est le point d’entrée principal de JPA car c’est via un EntityManager qu’on accède aux méthodes de sauvegarde, de recherche, de suppression… dans la base de données. 3. L’application L’application utilisée pour illustrer l’utilisation de JPA permet à des particuliers de louer des voitures. Elle dispose d’une interface utilisateur basique dans laquelle on saisit le nom d’une personne désirant louer une voiture définit par son numéro d’immatriculation : Page 1 [email protected] Le code de cette interface graphique occupe la couche Présentation dans l’architecture de l’application (voir la figure suivante). 4. La couche Application L’interface graphique accède à la couche Application via une interface : package application; public interface Location { public void louer(String nomPersonne, String immatriculation) throws VoitureException; public void ajouterVoiture(String immatriculation) throws VoitureException; /** * * @param nomPersonne * @return Le numero d'immatriculation de la voiture louee par nomPersonne ou null si la personne n'a pas louee de voiture Page 2 [email protected] * @throws PersonneException si il n'y a pas de personne ayant le nom nomPersonne */ public String rechercheVoiture(String nomPersonne) throws PersonneException; } Une implantation est bien évidement associée à cette interface : la classe LocationImpl qui n’est pas détaillée ici. 5. La couche métier (les classes persistantes) La couche Métier est composé de 2 classes, Voiture et Personne associées entre elles par une association de 1 vers plusieurs. Ce sont ces 2 classes qui vont être mappées en base de données avec 2 tables. Voici la partie principale du code de la classe Personne : package business; @Entity public class Personne { Voiture voiture; private long id; @ManyToOne public Voiture getVoiture() { return voiture; } public void setVoiture(Voiture voiture) { this.voiture = voiture; } @TableGenerator( name = "personneGen", table="PERSONNE_SEQUENCE_GENERATOR", pkColumnName="GEN_KEY", valueColumnName = "GEN_VALUE", pkColumnValue = "PERSONNE_ID", allocationSize = 10) @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "personneGen") public long getId() { return id; } public void setId(long id) { Page 3 [email protected] this.id = id; } } On remarque la présence d’annotations telles que @Entity, @ManyToOne ou @Id . C’est grâce à ces annotations que JPA peut générer automatiquement une table dans une base de données ainsi que le code JDBC pour y accéder : l’annotation @ManyToOne permet de traduire des associations entre objets d’un diagramme de classes en relation entre table dans la base de données ; l’annotation @Id est obligatoire et sert à définir quel champ de la classe sera mappé avec la clef primaire de la table. Ici, on demande à JPA de générer automatiquement les valeurs pour les clefs primaires grâce à l’annotation @TableGenerator mais c’est optionnel. 6. La couche Services (où est géré la persistance) La couche Application accède à la couche Services via une interface en utilisant le pattern Data Access Object (DAO) : package services; import business.Personne; import business.Voiture; public interface DAO { public void createVoiture(Voiture voiture); public void createPersonne(Personne personne); public Personne retrievePersonne(String nom); public Voiture retrieveVoiture(String numImmatriculation); public Voiture retrieveVoiture(Personne personne); public void updateVoiture(Voiture voiture); public void updatePersonne(Personne personne); public void removeVoiture(Voiture voiture); public void removePersonne(Personne personne); } Ce pattern DAO est très utilisé dans les applications JEE en général. C’est dans l’implantation de cette interface que toute la persistance est gérée. Voici un extrait de cette implantation : package services; public class DOAImpl implements DAO{ EntityManager em = MyEntityManagerFactory.getEntityManager(); // … public void createVoiture(Voiture voiture) { EntityTransaction tx = em.getTransaction(); tx.begin(); em.persist(voiture); tx.commit(); } Page 4 [email protected] public void removeVoiture(Voiture voiture) { EntityTransaction tx = em.getTransaction(); tx.begin(); em.remove(voiture); tx.commit(); } public Personne retrievePersonne(String nom) { List liste = em.createQuery( "SELECT p FROM Personne p WHERE p.nom LIKE :nomPersonne" ). setParameter( "nomPersonne", nom ). getResultList(); if(liste.size() == 0){ return null; } return (Personne)liste.get(0); } } Le code des méthodes createVoiture et removeVoiture ressemble à celui de l’introduction de ce document. La partie la plus intéressante, est contenue dans la méthode retrievePersonne. On y voit une requête qui est envoyée à la base de données : "SELECT p FROM Personne p WHERE p.nom LIKE :nomPersonne" De fait, JPA possède un langage proche du SQL pour extraire de la base de données un ensemble d’objets. 7. JPA + Spring Dans cette application, nous utilisons aussi Spring (bien que JPA puisse être utilisé sans Spring dans une application Java Standard Edition ou dans une application à la norme EJB). Spring est utilisé pour assembler les couches de l’application. Le fichier XML applicationContext.xml, conforme à la syntaxe Spring, où est défini cet assemblage est reprit en partie ici : <bean id="louerVoitureDialog" class="presentation.LouerVoitureDialog"> <property name="location"> <ref bean="location"/> </property> </bean> <bean id="location" class="application.LocationImpl"> <property name="dao"> <ref bean="dao"/> </property> </bean> <bean id="dao" class="services.DOAImpl"/> Nous y retrouvons les trois beans correspondant aux implantations des couches Présentation, Application et Services respectivement. Page 5 [email protected] 8. La configuration de JPA Il ne reste plus à présent qu’à étudier la configuration de JPA. Cela consiste principalement à choisir une base de données et une implantation de JPA. N’importe quelle base de données qui dispose d’un pilote JDBC de bonne qualité convient. Il y a plusieurs implantations de JPA : deux implantations couramment utilisées sont celle d’Hibernate et de OpenJPA. Ici, nous utilisons Hibernate 3. La configuration se fait essentiellement dans le fichier persistence.xml qui doit être placé dans un répertoire META-INF, lui-même placé dans le répertoire qui contient les sources de l’application. Voici un extrait de ce fichier : <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="manager1" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>java:/DefaultDS</jta-data-source> <class>business.Voiture</class> <shared-cache-mode>NONE</shared-cache-mode> <validation-mode>CALLBACK</validation-mode> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletonEhCacheProvider"></property> <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver"/> <property name="javax.persistence.jdbc.user" value="sa"/> <property name="javax.persistence.jdbc.password" value=""/> <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:."/> <property name="hibernate.max_fetch_depth" value="3"/> </properties> </persistence-unit> </persistence> Des explications détaillées sur cette configuration sont données dans la documentation d’Hibernate. Comme indiqué dans l’exemple au début de ce document, le point d’entrée de JPA est la classe EntityManager. Bien que Spring ne soit pas indispensable pour récupérer un EntityManager, il fournit des Helper Classes pour cela. Comme d’habitude avec Spring, c’est en XML qu’on définit quelles classes du framework on souhaite utiliser. Voilà le second extrait du fichier (applicationContext.xml) de définition des composants de Spring : <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:hsql://localhost/"/> <property name="username" value="sa"/> Page 6 [email protected] <property name="password" value=""/> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="persistenceUnitName" value="manager1" /> </bean> On y voit la déclaration de deux beans : le premier définit la source de données utilisée (ici c’est la base de données HSQLDB en local) ; le deuxième est la fabrique d’EntityManager. Cette fabrique s’utilise de la façon suivante : ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); EntityManagerFactory emf = (EntityManagerFactory)context.getBean("entityManagerFactory"); em = emf.createEntityManager(); 9. Déploiement et lancement de l’application sous Eclipse Les codes sources de ce projet ainsi que les libraires utiles (Hibernate, Spring, la base de données HSQLDB) sont à l’adresse : perso.efrei.fr/~charroux/JPA+Spring/ Après avoir lancé Eclipse, il faut commencer par créer un projet Java (File -> New -> Java Project) : Une fois le projet créé, décompressez les sources et les librairies dans le répertoire du projet. Rafraichissez votre projet pour qu’Eclipse prenne en compte l’ajout du code (File –> Refresh). Votre projet doit se présenter ainsi dans Eclipse : Page 7 [email protected] On y voit : - tous les packages correspondant aux couches de l’application (application, business, presentation…) ; le répertoire META-INF qui contient le fichier persistence.xml ; le fichier de déclaration des beans Spring applicationContext.xml ; les libraries .jar. Avant de lancer l’application, il faut démarrer la base de données. Ceci se fait en ligne de commande. Sous Windows par exemple, cela se fait de la façon suivante : Page 8 [email protected] Le répertoire où exécuter cette commande est le répertoire lib du projet. La dernière étape consiste à lancer l’application (voir la classe Test dans le répertoire test) : package test; public class Test { public static void main(String[] args) { Démarrage du conteneur léger de spring : ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Accès à la couche application via son interface : Location location = (Location)context.getBean("location"); Création d’une voiture dans la base de données : try { location.ajouterVoiture("23 AA 94"); } catch (VoitureException e) { System.out.println(e.getMessage()); } Lancement de l’interface utilisateur : LouerVoitureDialog louerVoitureDialog = (LouerVoitureDialog)context.getBean("louerVoitureDialog"); louerVoitureDialog.setVisible(true); } } Page 9