Voici un article qui intéressera plus les débutants, et qui intéressera aussi ce qui cherchent un petit exemple afin de tester JPA 2, l’implémentation d’Hibernate 3.5 et Apache Derby. Je vous parlerai aussi de la librairie Google Guava, qui permet de simplifier le code de son entité. Bref un petit article sans prétentions, à lire sur la plage.
Maven
Voyons comment créer un petit projet simple avec JPA. J’utilise Apache Derby, une base en mémoire qui permet de tester du code simple sans avoir besoin d’une vraie base Oracle ou MySQL. Du côté des dépendances, je vous déconseille slf4j-simple. Prenez plutôt slf4j-log4j12 afin de pouvoir contrôler la verbosité des logs d’Hibernate. J’utilise Apache Derby 10.5.3.0, et j’ai ajouté le repository de JBoss.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.letouilleur.sample</groupId> <artifactId>sample</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>Articles du Touilleur Express</name> <url>https://touilleur-express.fr/</url> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.1</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>jboss</id> <name>JBoss repository</name> <url>http://repository.jboss.org/maven2</url> </repository> </repositories> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>r06</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.3.2.GA</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <version>10.5.3.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.5.1-Final</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.5.6</version> </dependency> </dependencies> </project>
Persistence
Dans le répertoire testresourcesMETA-INF j’ai placé un fichier persistence.xml. Je déclare explicitement mon entité org.letouilleur.sample.Instrument
<?xml version="1.0" encoding="UTF-8"?> <persistence version="1.0" 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"> <persistence-unit name="testPU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>org.letouilleur.sample.Instrument</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="hibernate.connection.url" value="jdbc:derby:memory:unit-testing-jpa"/> <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.EmbeddedDriver"/> <property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect"/> <property name="hibernate.hbm2ddl.auto" value="create"/> <property name="hibernate.connection.username" value=""/> <property name="hibernate.connection.password" value=""/> </properties> </persistence-unit> </persistence>
Test avec JPA
Pour tester l’entité, j’ai fait le choix d’écrire le minimum de code et de ne pas utiliser Spring. J’ai créé une classe abstract pour tout ce qui est mise en marche du contexte de persistence, puis un test simple pour valider la création de mon entité. Pas de DAO non plus, l’EntityManager est un DAO à part entière non ?
Le test de base:
package org.letouilleur.sample; import junit.framework.TestCase; import org.apache.derby.impl.io.VFMemoryStorageFactory; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import java.io.File; import java.sql.DriverManager; import java.sql.SQLNonTransientConnectionException; import java.util.logging.Logger; /** * A simple Persistence Unit test. * * @author Nicolas Martignole * @since 5 août 2010 21:53:10 */ public abstract class PersistenceTest extends TestCase { private static Logger logger = Logger.getLogger(PersistenceTest.class.getName()); private EntityManagerFactory emFactory; protected EntityManager em; public PersistenceTest(){ } public PersistenceTest(String testName) { super(testName); } @Override protected void setUp() throws Exception { super.setUp(); try { logger.info("Starting in-memory database for unit tests"); Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); DriverManager.getConnection("jdbc:derby:memory:unit-testing-jpa;create=true").close(); } catch (Exception ex) { ex.printStackTrace(); fail("Exception during database startup."); } try { logger.info("Building JPA EntityManager for unit tests"); emFactory = Persistence.createEntityManagerFactory("testPU"); em = emFactory.createEntityManager(); } catch (Exception ex) { ex.printStackTrace(); fail("Exception during JPA EntityManager instanciation."); } } @Override protected void tearDown() throws Exception { super.tearDown(); logger.info("Shuting down Hibernate JPA layer."); if (em != null) { em.close(); } if (emFactory != null) { emFactory.close(); } logger.info("Stopping in-memory database."); try { DriverManager.getConnection("jdbc:derby:memory:unit-testing-jpa;shutdown=true").close(); } catch (SQLNonTransientConnectionException ex) { if (ex.getErrorCode() != 45000) { throw ex; } // Shutdown success } VFMemoryStorageFactory.purgeDatabase(new File("unit-testing-jpa").getCanonicalPath()); } }
Le test InstrumentJPATest me permet de tester la création de mon entité :
package org.letouilleur.sample; /** * A simple JPA integration test to demonstrate equals and hashCode issues. * * @author Nicolas Martignole * @since 5 août 2010 22:24:29 */ public class InstrumentJPATest extends PersistenceTest { public void testPersistence() { try { em.getTransaction().begin(); Instrument instrument = new Instrument("FR1234567890"); em.persist(instrument); assertTrue(em.contains(instrument)); em.remove(instrument); assertFalse(em.contains(instrument)); em.getTransaction().commit(); } catch (Exception ex) { em.getTransaction().rollback(); ex.printStackTrace(); fail("Exception during testPersistence"); } } }
Entité
Nous sommes maintenant prêt à créer notre Entité. Je vous propose 2 versions : une simple et une différente avec Google Guava.
Dans la spécification JSR-317 de JPA2, notez les points suivants :
Extrait de la spécification JSR-317:
The entity class must be annotated with the Entity annotation or denoted in the XML descriptor as an entity.
The entity class must have a no-arg constructor. The entity class may have other constructors as well. The no-arg constructor must be public or protected.
The entity class must be a top-level class. An enum or interface must not be designated as an entity.
The entity class must not be final. No methods or persistent instance variables of the entity class may be final.
If an entity instance is to be passed by value as a detached object (e.g., through a remote interface), the entity class must implement the Serializable interface.
Entities support inheritance, polymorphic associations, and polymorphic queries.
Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.
The persistent state of an entity is represented by instance variables, which may correspond to Java- Beans properties. An instance variable must be directly accessed only from within the methods of the entity by the entity instance itself. Instance variables must not be accessed by clients of the entity. The state of the entity is available to clients only through the entity’s methods—i.e., accessor methods (get- ter/setter methods) or other business methods.
En résumé:
– votre Entité peut soit utiliser une annotation, soit être configurée via XML
– elle doit avoir un constructeur public ou protected sans argument
– la classe doit être une « top-level class », il n’est pas possible d’utiliser un enum ou une interface.
– la classe ne doit pas être finale
– nécessité d’implémenter l’interface Serializable si l’object doit être détaché
package org.letouilleur.sample; import javax.persistence.*; import java.io.Serializable; /** * Instrument is a simple JPA for http://touilleur-express.fr * * @author Nicolas Martignole * @since 5 août 2010 21:47:50 */ @Entity public class Instrument implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false,length = 12) private String isin; @Column(nullable = true,length = 150) private String description; // Required for JPA2 protected Instrument() { } public Instrument(final String isin) { this(isin,null); } public Instrument(final String isin, final String description) { if (isin == null) { throw new NullPointerException("ISIN cannot be null"); } this.isin = isin; this.description = description; } public Long getId() { return id; } public String getIsin() { return isin; } public String getDescription() { return description; } }
Notez l’absence de setter. Il n’est pas nécessaire d’en déclarer car j’utilise (et je préfère) la déclaration des annotations sur les propriétés plutôt que sur les méthodes. Cela permet de regrouper au début de mon fichier l’ensemble des annotations, et de ne pas devoir chercher d’éventuelles contraintes
Ajoutons les méthodes toString, equals et hashCode. Etant donné que l’ISIN est une clé métier, je vais m’en servir pour gérer l’identité de mon Entité. Je modifie aussi le constructeur afin d’utiliser la classe Preconditions de Google Guava. Cela me permet d’alléger le test sur l’ISIN. Pensez-y lorsque vous avez une méthode avec 5 ou 6 arguments obligatoires. Guava est pratique et permet d’économiser du code.
Pour la partie equals et hashCode, je m’appuie sur l’ISIN qui est une clé métier. Cela m’arrange bien, pas besoin de gérer l’id qui peut être null tant que l’entité n’est pas persisté.
package org.letouilleur.sample; import com.google.common.base.Objects; import static com.google.common.base.Preconditions.*; import javax.persistence.*; import java.io.Serializable; /** * Instrument is a simple JPA for http://touilleur-express.fr * * @author Nicolas Martignole * @since 5 août 2010 21:47:50 */ @Entity @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"ID", "ISIN"})) public class Instrument implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false, length = 12, name = "ISIN") private String isin; @Column(nullable = true, length = 150) private String description; // Required for JPA2 protected Instrument() { } public Instrument(final String isin) { this(isin, null); } public Instrument(final String isin, String description) { checkNotNull(isin, "Isin cannot be null"); // Google Guava this.isin = isin; this.description = description; } public Long getId() { return id; } public String getIsin() { return isin; } public String getDescription() { return description; } @Override public String toString() { // Google Guava return Objects.toStringHelper(this).add("id", getId()).add("isin", getIsin()).add("description", getDescription()).toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Instrument that = (Instrument) o; return Objects.equal(that.isin, isin); // Google Guava } @Override public int hashCode() { return Objects.hashCode(isin); // Google Guava } }
Soyons fou
Prenons une autre approche un peu différente. Si je vous montre d’abord le code du test unitaire, réfléchissez sur l’implémentation de la classe InstrumentV2 afin qu’elle passe le test unitaire. Vous verrez, c’est un exercice intéressant.
package org.letouilleur.sample; import junit.framework.TestCase; import java.util.List; public class InstrumentV2UnitTest extends TestCase { public void testShouldCreateAndFindAnEntity() { InstrumentV2 instrument = new InstrumentV2("FR123456"); InstrumentV2 instrument2 = new InstrumentV2("US94394949"); InstrumentV2.persist(instrument); InstrumentV2.persist(instrument2); List<InstrumentV2> results=InstrumentV2.findAll() ; assertEquals(2,results.size()); assertEquals(instrument, InstrumentV2.find("FR123456")); } }
A la lecture du code, nous voyons donc des méthodes statiques déclarées sur l’entité. A votre avis qu’est-ce que cela donne ? Et bien c’est faisable. Il y a plusieurs approches différentes, mais j’ai fait cela à ma manière, avec un bloc static pour créer le contexte de persistence. Je pense que l’annotation @PersistenceContexte ne servira à rien, mais je la laisse là.
package org.letouilleur.sample; import com.google.common.base.Objects; import javax.persistence.*; import java.io.Serializable; import java.sql.DriverManager; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; /** * Instrument is a simple JPA for http://touilleur-express.fr * * @author Nicolas Martignole * @since 5 août 2010 21:47:50 */ @Entity @NamedQuery(name = "byIsin", query = "FROM InstrumentV2 I WHERE I.isin=:pisin") @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"ID", "ISIN"})) public class InstrumentV2 implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false, length = 12, name = "ISIN") private String isin; @Column(nullable = true, length = 150) private String description; @PersistenceContext static EntityManager em; static { System.out.println("--- Initializing context ---"); try { Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); DriverManager.getConnection("jdbc:derby:memory:unit-testing-jpa;create=true").close(); } catch (Exception ex) { ex.printStackTrace(); } try { EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("testPU"); em = emFactory.createEntityManager(); } catch (Exception ex) { ex.printStackTrace(); } } // Required for JPA2 protected InstrumentV2() { } public InstrumentV2(final String isin) { this(isin, null); } public InstrumentV2(final String isin, String description) { checkNotNull(isin, "Isin cannot be null"); this.isin = isin; this.description = description; } public Long getId() { return id; } public String getIsin() { return isin; } public String getDescription() { return description; } @Override public String toString() { return Objects.toStringHelper(this).add("id", getId()).add("isin", getIsin()).add("description", getDescription()).toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InstrumentV2 that = (InstrumentV2) o; return Objects.equal(that.isin, isin); } @Override public int hashCode() { return Objects.hashCode(isin); } public static InstrumentV2 find(String isin) { Query q = em.createNamedQuery("byIsin").setParameter("pisin", isin); return (InstrumentV2) q.getSingleResult(); } public static void persist(InstrumentV2 instrument) { checkNotNull(instrument); em.getTransaction().begin(); em.persist(instrument); em.getTransaction().commit(); } @SuppressWarnings("unchecked") public static List<InstrumentV2> findAll(){ return em.createQuery("from InstrumentV2").getResultList(); } }
Si vous voulez tester, n’oubliez pas d’ajouter la class InstrumentV2 dans la déclaration de persistence du fichier persistence.xml. Et vous verrez, cela marche très bien !
C’est une approche similaire, quoiqu’un peu plus compliquée, qu’utilise Play! Framework. Attention, Play utilise un système d’enrichissement du code à la compilation qui permet de donner une implémentation des méthodes type find, save, delete, mais certainement pas quelque chose avec un bout de code static comme je l’a fait ici.
Je rigole car vous vous êtes dit : « tiens il ne parle pas de Play!« . Et alors que Madame vous tartine le dos pendant que vous lisez ce billet sur votre iPad sur la plage, PAF au moment où vous ne vous y attendez pas, je vous reparle de Play… Avouez que je vous ai bien eu non ?
Allez, je vais vous laisser tranquille, passez de bonnes vacances.
0 no like
Hello,
Je viens de la parcourir rapidement… (j’approfondirai demain matin non pas sur la plage mais dans le train) … très intéressant!
D’autant plus intéressant, qu’actuellement je travaille sur un petit projet perso dans lequel j’utilise JPA2..
THX,
J’ai grillé le côté Play! dès le test unitaire avec InstrumentV2.find() !
Par contre, j’ai pas mal de lacunes java… qui peut m’expliquer dans la V2 le gros bloc static {} qui initialise la persistence ? C’est un « constructeur static » ?
C’est exécuté à quel moment ?
Merci pour l’exemple !
salut,
J’ai une question qui m’embête concernant les tests de la couche de persistence, quand tu fais ton test
InstrumentV2UnitTest
tu ne vérifies pas que les données sont effectivement présentes en base. Tout pourrait ‘trainer’ dans le cache JPA et ne pas être effectivement enregistré (surtout qu’on en voit pas de gestion de la transaction).J’eusse aimé du JUnit 4 (pour faire hype ;o) ) et pour ne pas recharger fermer tout entre chaque test ‘unitaire’ mais plutôt 1 fois pour la classe de tests (et ainsi être un peu plus ‘performant’).
Guava ça a l’air vraiment génial, faut que je remplace mes commons-lang / io ….
@Julien: un bloc
static
est exécuté au chargement de la classe dans la VM (normal puisque c’est static; o) ).@Emmanuel : salut emmanuel. La transaction est gérée dans la méthode persist, qui est static… donc synchronisée… donc le test est bon. Oui pour JUnit 4, j’y ai pensé aussi. Pour Guava, le plus intéressant est à venir. Stay tuned !
Hello,
merci pour l’article.
La partie sur equals/hashcode permettra de rappeler un moyen fiable de les implémenter:lorsque c’est possible, repérer l’unicité métier d’un champ ou combinaison de champs, bien joué.
Par contre attention:
« Notez l’absence de setter. Il n’est pas nécessaire d’en déclarer car j’utilise (et je préfère) la déclaration des annotations sur les propriétés plutôt que sur les méthodes. Cela permet de regrouper au début de mon fichier l’ensemble des annotations, et de ne pas devoir chercher d’éventuelles contraintes »
L’endroit de l’annotation (propriété ou méthode) a un impact non négligeable, ben vu…
Si l’approche « findAll statique » te plait, tu peux aussi essayer Fonzie, qui t’économisera quelques lignes de code 😛
Hé ça me plait bien le findAll statique! Ça me fait penser à l’approche playframework, bien DDD sans couche service!
@Nicolas De loof je vais regarder fonzie merci 🙂
« Notez l’absence de setter »
Dans la première version de l’entité Instrument il manque un constructeur, on ne peut pas mettre de description 😉
L’entity-manager d’Hibernate apparaît en double (avec 2 versions 3.3.2.GA et 3.5.1-Final) dans les dépendances Maven, une coquille ou est-ce volontaire ?
@frederic C’est volontaire, ils ne sont pas dans le même scope maven.
@guillaume carré : ok je corrige, merci 🙂
Désolé je vais faire mon rabat-joie mais, si l’on prend
public static void persist(InstrumentV2 instrument)
1- La méthode gère elle-même la transaction.
DDD ou pas, comment coder un use case qui va nécessiter, d’un point de vue transactionnel, qu’un InstrumentV2 soit rendu persistant en même temps qu’un AnotherEntity ? En gros comment respecter l’atomicité?
C’est pas déconnant, sans aller jusqu’à l’appli monstrueuse, ce sont des use-case qui ont du sens non?
Quelle solution proposes tu dans ce contexte? Il n’y en a pas 36:
– soit on ajoute un méthode persist(InstrumentV2 instrument, AnotherEntity another). Et la mon gros problème est on la met ou cette méthode? Dans AnotherEntity ou dans InstrumentV2 ?
– soit on externalise dans un autre objet, non stéréotypé Entity et dans ce cas, que fait-on des méthodes déjà présentes dans les entités, qui ont un lien, ça devient un peu compliqué pour s’y retrouver non?
– enfin, la notion de ‘contexte de persistance’ devient assez aléatoire et difficile a gérer avec un tel pattern.
Attention, je ne suis pas pour un radicalisation en 36 couche, loin de là MAIS on se pose tres vite des questions lorsque l’EM est directement utilisé dans les entités. La transactionnel, sans parler de gros système ou autre, est assez rapidement nécessaire et dans ce cas, la délimitation de la transaction doit être des plus lisibles non?
Ce n’est que mon humble avis.
Anthony
@Nicolas Martignole « la méthode persist, qui est static donc synchronisée » => je n’ai pas compris le rapport avec la synchronisation.
Tu parles de synchro au sens Java ? De commit de la transaction ? Quel rapport avec static ? Peut-être as-tu un lien (javadoc ?) à partager à ce sujet.
Par ailleurs, je n’ai pas compris cette partie du code :
Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); DriverManager.getConnection("jdbc:derby:memory:unit-testing-jpa;create=true").close();
Visiblement, tu charges une classe, et tu ouvres une connexion que tu fermes immédiatement. Je suppose que c’est dans un but particulier, peut-être permettre de définir quel driver utiliser, et le configurer.
Une mini explication à ce sujet ne ferait pas de mal 😉 .
@Tous Une petite astuce dont j’ai déjà du parler ici : avez-vous trouvé ce code lisible ?
return Objects.toStringHelper(this).add("id", getId()).add("isin", getIsin()).add("description", getDescription()).toString();
Le save action de reformatage automatique des IDE fait que les lignes sont coupées quand elles dépassent les X caractères, et tant que ça n’est pas le cas elles sont remises sur une seule ligne.
En général, c’est bien pratique. Quand on réalise des appels chainés (fluid interface, DSL, pattern builder), c’est illisible.
L’astuce, c’est de formatter soit même, et de terminer les lignes par // pour éviter que l’IDE ne reformate. Ce qui donne :
return Objects.toStringHelper(this) // .add("id", getId()) // .add("isin", getIsin()) // .add("description", getDescription()) // .toString();
@Julien Durillon Allez jme lance dans les explications 🙂 . Un bloc static est exécuté au chargement de la classe, après qu’aient été initialisées les variables statiques.
A noter qu’en Java, les classes ne sont chargées que lorsqu’on en a besoin.
Si tu exécutes le code suivant, tu verras « hello dear world ».
public class TestStatic { static TestStatic t = new TestStatic(); static { System.out.println("dear"); } static { System.out.println("world"); } TestStatic() { System.out.println("hello"); } public static void main(String[] args) {} } </code
Après réflexion, pour les static, ce code serait peut-être plus intéressant :
public class TestStatic { static { System.out.println("dear"); } static { System.out.println("world"); } public static void main(String[] args) { System.out.println("hello !"); } }
En exécutant ce code, la méthode main est appelée. Avant même qu’elle ne soit appelée, la classe TestStatic est chargée (puis main lui appartient), et les bloc statiques sont exécutés.
Le résultat est donc :
dear
world
hello !
Hello,
« C’est volontaire, ils ne sont pas dans le même scope maven. »
@Nicolas : pourquoi les 2 versions sont différentes, malgrés la différence de scope?
Merci d’avance,
Un article complémentaire de celui-ci serait la création de la base de données SQL avant les tests.
PS: J’ai noté quelques erreurs, pas dans le code Java, mais dans les codes ISIN, qui ne sont pas valides. Tu aurais du utiliser des codes RICs 🙂
Les méthodes statiques sont aussi proposées par Spring Roo qui les génère automatiquement (find, findAll, remove, persist, merge, count) dans un fichier .aj à côté du .java de l’entité. C’est super pratique et rend le .java plus léger et plus lisible…
La notion de TypedQuery est aussi une nouveauté intéressante dans le spéc jpa 2.0, elle permet (entre autres) de se passer des annotations @SuppressWarning.
Par exemple :
@SuppressWarnings(« unchecked »)
public static List findAll(){
return em.createQuery(« from InstrumentV2 »).getResultList();
}
Deviendrait :
public static List findAll(){
return em.createQuery(« from InstrumentV2 », InstrumentV2.class).getResultList();
}
J’ai eu une chouette erreur de versions des libs slf4j-api tirées par Maven
Tout est expliqué ici : http://www.slf4j.org/faq.html#IllegalAccessError (impressionnant comme l’explication anticipe un cas précis)
hibernate-manager (j’ai voulu récupérer la version 3.4.0.GA) tire une dépendance vers slf4j-api:jar version 1.4.2
Or les versions <1.4.3 sont incompatibles avec les versions suivantes des bindings de slf4j (ici : slf4j-log4j12 version 1.5.6)
La solution est de fixer dans le pom.xml la version à utiliser de slf4j-api pour être tranquille en rajoutant ça :
org.slf4j
slf4j-api
1.5.6
Bonjour, merci pour l’article,equals/hashcode permettra de faire un rappele sur les implémenter et repérer l’unicité métier d’un champ ou combinaison de champs.