Je vous propose un petit article technique sur Maven2, JPA et plus particulièrement la gestion de plusieurs persistence.xml. Cet article intéressera les personnes qui ont un projet avec JPA, et qui souhaitent gérer une version pour le packaging final et une version pour les tests unitaires ou d’intégrations. J’ai eu ce cas lors de la mise en place de tests Fits, où nous souhaitions pouvoir utiliser un profil JPA pour les tests avec H2, capable de fonctionner en mode « base de données en mémoire/base de données serveur TCP », et un profil classique avec Oracle.
Gérer 2 datasources
Dans mon exemple, je souhaite gérer 2 datasources distinctes : une pour l’exécution de l’application, et une autre pour l’exécution des tests unitaires. Lors de l’exécution de mon application, mes classes sont annotées et j’ai donc aussi besoin que le moteur JPA trouve celles-ci, afin de les instrumenter. Une datasource utilise une base Oracle, une autre datasource utilise le mode « in-memory » d’h2.
Approche simple
La première approche est simple :
– écrire un fichier générique persistence.xml dans src/main/resources/META-INF
– déclarer des propriétés dans votre pom.xml
– éventuellement avoir un profil maven pour les tests et un profil pour l’exécution de l’application
<?xml version="1.0" encoding="UTF-8"?> <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_1_0.xsd" version="1.0"> <persistence-unit name="sample-db" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <properties> <property name="hibernate.dialect" value="${hibernate.dialect}"/> <property name="hibernate.hbm2ddl.auto" value="${jdbc.hbm2ddl.auto}"/> <property name="hibernate.show_sql" value="${jdbc.show_sql}"/> <property name="hibernate.format_sql" value="${jdbc.format_sql}"/> <property name="hibernate.connection.url" value="${jdbc.url}"/> <property name="hibernate.connection.driver_class" value="${jdbc.driver}"/> <property name="hibernate.connection.username" value="${jdbc.username}"/> <property name="hibernate.connection.password" value="${jdbc.password}"/> </properties> </persistence-unit> </persistence>
Le fichier pom.xml contiendra ensuite 2 profils. Un profil permet d’activer le mode « base de données en mémoire » avec H2, pratique pour les tests unitaires. Un deuxième mode permet de se connecter via TCP sur une instance d’H2 démarrée en local, pratique pour vérifier l’état de la base à posteriori, sans utiliser DbUnit pour l’instant.
Fichier pom.xml :
<project> ... ... <profiles> <profile> <id>h2-tcp-db</id> <properties> <jdbc.url>jdbc:h2:tcp://localhost/~/test_db;MODE=Oracle</jdbc.url> <jdbc.driver>org.h2.Driver</jdbc.driver> <jdbc.username>sa</jdbc.username> <jdbc.password></jdbc.password> <jdbc.format_sql>false</jdbc.format_sql> <jdbc.show_sql>false</jdbc.show_sql> <jdbc.hbm2ddl.auto>create</jdbc.hbm2ddl.auto> </properties> </profile> <profile> <id>h2-inmemory-db</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <jdbc.url>jdbc:h2:mem:test_db;MODE=Oracle;DB_CLOSE_ON_EXIT=FALSE</jdbc.url> <jdbc.driver>org.h2.Driver</jdbc.driver> <jdbc.username>sa</jdbc.username> <jdbc.password></jdbc.password> <jdbc.format_sql>false</jdbc.format_sql> <jdbc.show_sql>false</jdbc.show_sql> <jdbc.hbm2ddl.auto>create-drop</jdbc.hbm2ddl.auto> </properties> </profile> </profiles> </project>
Cela répond aux cas simples. Si par contre vous devez gérer plusieurs formats de fichier persistence.xml, cela se complique un peu. Il est possible de déclarer une datasource pour les tests, une autre pour l’exécution. Jusqu’ici pas de soucis. Mais avouez que packager une datasource de tests dans l’application finale, ce n’est pas top non ?
Approche avec 2 fichiers persistence.xml
J’ai cherché à configurer Maven et mon environnement afin d’avoir un fichier persistence.xml dans mon répertoire src/main/resources/META-INF pour la prod, et un deuxième fichier de persistence dans src/test/resources/META-INF pour mes tests unitaires et mes tests d’intégrations.
Armé de ma meilleur volonté, j’ai commencé par déposer un deuxième fichier persistence.xml dans le répertoire src/test/resources/META-INF en me disant que ce serait bon… Et bien non.
Le fichier est copié lors de la phase « test » comme prévu, les variables sont remplacées si vous n’avez pas touché au plugin maven-resource.
<!-- fichier pom.xml --> <project> ... ... <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <testResources> <testResource> <directory>src/test/resources</directory> <filtering>true</filtering> <excludes> <exclude>**/fit/**</exclude> <exclude>**/reports/**</exclude> </excludes> </testResource> </testResources> </build> ... </project>
Tout fonctionne du côté de Maven, mais par cotnre le mapping JPA ne fonctionne plus. En effet, le fichier étant déposé dans target/test-classes, et ce répertoire ne contenant pas mes entités annotées, JPAne peut rien faire. La solution semble donc de trouver le moyen de dire à JPA où trouver les classes (dans target/classes).
Le tag jar-file permet de spécifier l’emplacement d’un jar ou d’un répertoire différent pour JPA, qui sera utilisé afin d’y trouver les classes annotées.
Voici le fichier persistence.xml tel que je souhaite qu’il soit au final dans mon répertoire target/test-classes/META-INF :
<?xml version="1.0" encoding="UTF-8"?> <!-- Fichier final dans target/test-classes/META-INF --> <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_1_0.xsd" version="1.0"> <persistence-unit name="test_db" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jar-file>file:/C:/Dev/monprojet/target/classes</jar-file> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.hbm2ddl.auto" value="${jdbc.hbm2ddl.auto}"/> <property name="hibernate.show_sql" value="${jdbc.show_sql}"/> <property name="hibernate.format_sql" value="${jdbc.format_sql}"/> <property name="hibernate.connection.url" value="${jdbc.url}"/> <property name="hibernate.connection.driver_class" value="${jdbc.driver}"/> <property name="hibernate.connection.username" value="${jdbc.username}"/> <property name="hibernate.connection.password" value="${jdbc.password}"/> </properties> </persistence-unit> </persistence>
Notez que pour le tag jar-file, il faut utiliser une URI, pas un chemin technique. Ce qui va compliquer un peu notre travail dans quelques instants. La question est maintenant la suivante : comment remplacer file:/C:/Dev/monprojet/ par le répertoire de base de Maven ? le tout sous la forme d’une URL ?
Euréka, Maven depuis la version 2.1 propose une propriété project.baseUri qui correspond exactement à ce que je veux au final : file:/C:/Dev/monprojet/.
Je remplace alors le tout dans mon fichier test/resources/META-INF/persistence.xml comme suit (attention au dernier slash qui est ajouté systématiquement):
<?xml version="1.0" encoding="UTF-8"?> <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_1_0.xsd" version="1.0"> <persistence-unit name="test_db" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jar-file>${projectBaseUri}target/classes</jar-file> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.hbm2ddl.auto" value="${jdbc.hbm2ddl.auto}"/> <property name="hibernate.show_sql" value="${jdbc.show_sql}"/> <property name="hibernate.format_sql" value="${jdbc.format_sql}"/> <property name="hibernate.connection.url" value="${jdbc.url}"/> <property name="hibernate.connection.driver_class" value="${jdbc.driver}"/> <property name="hibernate.connection.username" value="${jdbc.username}"/> <property name="hibernate.connection.password" value="${jdbc.password}"/> </properties> </persistence-unit> </persistence>
Je lance mvn test, et je vais voir le résultat : ma variable n’est pas remplacée, elle reste positionnée à project.baseUri… Bon, vieux réflexe de gars qui connait bien Maven 2, allons voir sur les bugs s’il n’y aurait pas un beau bug dans la dernière version de Maven…
Trop facile, je trouve celui-ci : MRESOURCES-99 ${project.baseUri} and ${maven.build.timestamp} are not expanded by resource filtering
Il y a un workaround simple : déclarer la propriété dans votre pom.xml comme ci-dessous :
<properties> <timestamp>${maven.build.timestamp}</timestamp> <projectBaseUri>${project.baseUri}</projectBaseUri> </properties>
Et là enfin, tout fonctionne. Un petit « mvn test -Ph2-inmemory-db » et je lance mes tests en mémoire avec un fichier persistence.xml dédié.
Pour terminer, voici mon fichier persistence.xml final, tel qu’il est actuellement :
<?xml version="1.0" encoding="UTF-8"?> <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_1_0.xsd" version="1.0"> <persistence-unit name="test_db" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <!-- When using test mode, you must introspect the Entities in a different folder, so that create schema works --> <!-- There is a bug in maven 2.2.x http://jira.codehaus.org/browse/MRESOURCES-99 so I added a property in the main pomx.xml--> <jar-file>${projectBaseUri}target/classes</jar-file> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.hbm2ddl.auto" value="${jdbc.hbm2ddl.auto}"/> <property name="hibernate.show_sql" value="${jdbc.show_sql}"/> <property name="hibernate.format_sql" value="${jdbc.format_sql}"/> <property name="hibernate.connection.url" value="${jdbc.url}"/> <property name="hibernate.connection.driver_class" value="${jdbc.driver}"/> <property name="hibernate.connection.username" value="${jdbc.username}"/> <property name="hibernate.connection.password" value="${jdbc.password}"/> <!-- other hibernate properties for historization --> <property name="hibernate.ejb.event.post-insert" value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener"/> <property name="hibernate.ejb.event.post-update" value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener"/> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener"/> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener"/> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener"/> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener"/> </properties> </persistence-unit> </persistence>
Conclusion
Voilà, je ne sais pas si cela pourra vous servir. Je me sers aussi du Touilleur Express comme d’un cahier où je note ce que je trouve, c’était l’idée au départ du blog. Si vous avez d’autres idées, n’hésitez pas à contribuer via les commentaires.
Cool. Merci pour l’astuce.
Sinon, pour ma part (et je ne sais pas si cela peut répondre à ta problématique), j’avais utilisé les classifier pour générer des jars différents et avoir une gestion plus fine au niveau de mon scope.
J’ai essayé d’en parler sur mon blog : http://jetoile.blogspot.com/2010/01/retour-sur-la-mise-en-uvre-d_04.html
Hope this helps… 😉
A noter aussi que si on utilise spring, on est pas obliger de mettre la la conf de connexion à la base dans le persistence.xml. On peut la laisser dans les fichiers de conf spring, et donc avoir une conf de test différente.
Cool, je connaissais pas le coup du .
Apparemment Hibernate est assez souple quant à son utilisation, chez moi ça marche directement avec un chemin absolu :
${project.build.outputDirectory}
Arf l\’aime pas les tags dans le commentaire.
Je parlais bien evidemment du tag jar-file
Dans le même esprit que ce genre de situation, on développe en ce moment un outil qui permettrait de gérer les évolutions du model, et ceci indépendamment de la BDD choisie.
Le projet est basé sur les dialect hibernate et tapestry-ioc (non pas tapestry-core qui est le framework web) et on devrait arriver à fournir un plugin maven.
C’est largement inspiré des migrations de Ruby on Rails.
http://github.com/spreadthesource/tapestry5-db-migrations
@Robin ça c’est un vrai besoin, je ne comprends pas que ça ne soit pas plus adressé pour le moment dans le monde Java.
Le jour où j’ai découvert les migrations de BD avec RoR, j’ai trouvé ça génial. Et étrange de ne le retrouver nul part ailleurs… Une de ses grandes forces, c’est d’utiliser un DSL en ruby pour décrire le schéma de donnée.