Cet article a pour but de vous donner une introduction rapide de JPA 2 ainsi qu’un exemple de mise en place avec Spring.
JPA (Java Persistance API) est la norme JEE permettant de faire de la persistance en base de données. En d’autres termes, cela va permettre de sauvegarder les objets contenant vos données en base à l’aide d’une API normalisée et ce quelle que soit la base de données utilisée. Il s’agit du successeur d’Hibernate.
Spring va nous servir pour faire l’injection de dépendances (IoC). Je devrais pour bien faire parler de CDI qui est la norme JEE pour l’IoC (au même titre que JPA pour la persistance). Cependant, je n’ai pas encore eu le temps de me pencher dessus. Comme j’avais déjà la version Spring de fonctionnelle, j’ai préféré partir là dessus, l’adaptation à CDI ne devrait cependant pas poser trop de soucis.
Je vous conseille de récupérer le code, si vous avez git via un :
git clone git://github.com/alexthomazo/jpa2-spring.git
soit en téléchargeant l’archive ici ou encore en visualisation directe là.
Le modèle
Le modèle utilisé comme exemple est celui d’une application permettant de gérer des albums photos ainsi que leur visionnage par des utilisateurs. Voici le modèle physique de données :
Les users appartiennent à un ou plusieurs groups. Chaque groupe a accès à un ou plusieurs photo_albums. Cependant, les utilisateurs peuvent avoir des privilèges particuliers, a savoir avoir un accès à un album en particulier (via la table photo_album_user_allowed) ou se voir refuser l’accès malgré son appartenance à un groupe ayant accès (photo_album_user_denied). Chaque photo_albums est composé d’un ou plusieurs album_items (photo ou vidéo).
Le script de création des tables se trouve dans src/main/db/schema.sql.
La configuration JPA
La première étape va consister à définir l’implémentation utilisée pour JPA. En effet, comme je le disais en introduction, JPA n’est qu’une API. Elle ne réalise rien en tant que tel. Il existe donc plusieurs implémentation de l’API tel que Hibernate de JBoss ou OpenJPA d’Apache.
Pour ce faire, il suffit de créer un fichier persistence.xml dans le répertoire META-INF du classpath (à l’aide de Maven, il sera donc logiquement dans src/main/resources).
Le contenu du fichier en lui même est assez succinct, il sert principalement à définir l’implémentation à utiliser (ici Hibernate) :
<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="appdb" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
</persistence-unit>
</persistence>
Il ne reste plus qu’à rajouter la dépendance dans le pom.xml :
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.0.Final</version>
</dependency>
Le mapping
Afin de faire correspondre notre modèle de donnée avec nos classes Java, nous allons devoir indiquer à JPA comment réaliser le mapping entre nos tables, nos classes et nos attributs. Pour cela, des annotations sont à notre disposition. Prenons un exemple simple avec la classe Group (les classes sont dans le package org.alexthomazo.blog.model.db) :
@Entity
@Table(name="groups")
public class Group {
private int groupId;
private String title;
private Set users;
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="group_id")
public int getGroupId() {
return groupId;
}
public String getTitle() {
return title;
}
@ManyToMany(mappedBy="groups")
public Set getUsers() {
return users;
}
/* + setters */
}
La première annotation est @Entity qui déclare la classe comme un objet persistant. Elle est complétée par @Table qui précise le nom réel de la table dans la base de données. Si la classe et la table ont le même nom, elle peut être omise.
Le reste du mapping se fait ensuite de manière automatique en fonction des getters définis dans la classe. JPA mappe par défaut les attributs comme dans cet exemple avec title. Si l’on souhaite ajouter un getter qui n’a pas lieu d’être persisté, il faut l’annoter avec @Transient.
Certains attributs ont des annotations permettant de modifier leurs comportements et sont positionnées sur leurs getters. Par exemple, groupId a une annotation précisant son rôle de clé primaire (@Id), le fait que cette clé primaire est générée automatiquement par la base de données sous-jacente (@GeneratedValue) et le nom de la colonne en base (@Column).
Prenons maintenant l’exemple du mapping camera_owner qui lie un item à un user. Il s’agit donc ici d’un lien 1-n (un user associé à un ou plusieurs item). Dans la classe AlbumItem, nous avons :
@ManyToOne
@JoinColumn(name="camera_owner")
public User getCameraOwner() {
return cameraOwner;
}
Nous définissons le type de relation à l’aide de @ManyToOne (ici 1-n). Puis nous spécifions le nom de la colonne qui sert pour la jointure avec @JoinColumn.
Le principe est le même concernant les relations n-n. Voyons l’exemple entre user et group (un user peut être dans plusieurs groupes et un groupe contient plusieurs users). Dans la classe User, nous avons :
@ManyToMany
@JoinTable(name="user_group",
joinColumns=@JoinColumn(name="user_id"),
inverseJoinColumns=@JoinColumn(name="group_id"))
public Set getGroups() {
return groups;
}
Cette fois-ci nous spécifions une relation n-n avec @ManyToMany. Puis nous indiquons la table faisant le mapping entre user et group avec @JoinTable. Le paramètre joinColumns sert à préciser la colonne représentant la classe courante dans la table de jointure tandis que inverseJoinColumns permet de spécifier la colonne correspondant à l’extrémité de la relation (ici Group).
Nous pouvons alors spécifier dans la classe Group la relation inverse :
@ManyToMany(mappedBy="groups")
public Set getUsers() {
return users;
}
Étant donné que la relation a déjà été entièrement décrite dans la classe User, il suffit ici de spécifier uniquement l’attribut auquel on fait référence dans la classe de destination à l’aide de @ManyToMany.
Voici pour les grandes lignes du mapping avec JPA. Bien sûr il existe d’autres annotations et d’autres paramètres pour faire bien plus que l’exemple, tout est indiqué dans la spécification JPA (voir a la fin de l’article pour le lien). Je fait aussi l’impasse sur les stratégies de chargement, lazy loading et autre joyeusetés, il s’agit de donner ici une première vue sur JPA.
Le méta-modèle
Maintenant que nos tables sont mappées avec nos classes, nous voulons pouvoir effectuer des requêtes sur celles-ci afin de récupérer nos données, les modifier, etc…
Il existe deux mécanismes de requêtage avec JPA : JPQL et l’API Criteria. Le premier s’approche plus d’un système comme SQL où l’on va énoncer sa requête sous forme textuelle :
SELECT u FROM User u
L’autre façon, l’API Criteria, va nous permettre d’énoncer les requêtes programmatiquement afin de valider la cohérence de celles-ci à la compilation à l’aide de la vérification des types des variables. C’est ce système que j’ai choisi d’utiliser.
Cependant nous allons avoir un soucis. En effet, comment faire pour exprimer nos conditions ? Si nous appelons l’accesseur de notre classe, nous allons récupérer la valeur de l’attribut et non sa représentation. Par exemple, si nous voulons récupérer la liste des utilisateurs dont le prénom est Alice, il va falloir trouver un moyen d’exprimer la représentation de l’attribut firstname et non sa valeur (une String en l’occurrence que l’on récupérerais avec getFirstname()).
Afin de pallier à ce problème, JPA introduit la notion de méta-modèle. Ce méta-modèle généré à partir de notre mapping va permettre de décrire les classes et les attributs de notre modèle. Concrètement, chaque classe va se voir créer une classe correspondante postfixée avec un underscore (User aura une classe User_, Group une classe Group_, etc…).
Si l’on regarde les classes ainsi générées, on remarque que pour chaque attribut de notre modèle, un attribut de classe correspondant a été créé. Par exemple pour Group :
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Group.class)
public abstract class Group_ {
public static volatile SingularAttribute groupId;
public static volatile SetAttribute users;
public static volatile SingularAttribute title;
}
Les attributs ont un type spécifique générique comprenant la classe d’appartenance et le type de l’attribut source. Ainsi, le compilateur va pouvoir vérifier la cohérence de nos requêtes Criteria car il dispose d’un moyen d’exprimer le type d’un attribut.
La configuration de la génération de ce méta-modèle s’effectue dans le pom.xml au niveau des balises <build>. Grâce au plugin m2e-apt, il nous suffit de configurer la compilation du projet avec une JRE au minimum 6 pour la gestion des annotations (ici 7). Il faut aussi ajouter la dépendance suivante qui contient le code qui génère tout ça :
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.2.0.CR1</version>
</dependency>
Ne pas oublier d’installer le plugin m2e-apt via les connecteurs Maven (Window -> Preferences -> Maven -> Discovery -> Open Catalog -> m2e-apt) puis de l’activer après redémarrage d’eclipse (Window -> Preferences -> Maven -> Annotation Processing -> Automatically configure JDT APT).
S’il n’est pas disponible dans le catalogue, il est possible de l’ajouter via le site Eclipse suivant : http://download.jboss.org/jbosstools/updates/m2e-extensions/m2e-apt
À ce stade, nous avons un modèle de classes représentant nos objets en base de données ainsi qu’un méta-modèle nous permettant d’effectuer des requêtes sur ce modèle, voyons comment.
Les DAO
Les DAO vont contenir le code permettant d’effectuer les requêtes sur la base de données. Dans l’exemple, ils sont dans le package org.alexthomazo.blog.model.dao :
J’ai choisi de garder le modèle interface/implémentation dans l’optique de pouvoir changer si besoin de JPA vers un autre système. Ce système n’est pas forcément très pertinent (à mon avis le changement ne se fera jamais) mais à le mérite de montrer l’utilisation d’une interface/implémentation avec un système d’injection de dépendances.
Chaque DAO commence donc par définir une interface (ex: IUserDao). Afin d’éviter de réécrire les mêmes méthodes de base a chaque fois (get, update, list, etc…), chaque interface hérite de l’interface AbstractDao. Cette interface générique permet de définir un certain nombre de méthodes de base à implémenter.
L’implémentation passe donc par la création d’une classe dans le sous-package jpa. Afin de ne pas ré-implémenter les méthodes de base définie par AbstractDao à chaque fois, on hérite de la classe d’implémentation AbstractJPADAOImpl. Cette classe contient l’implémentation des méthodes de base ce qui permet de gérer dans notre implémentation que les méthodes spécifiques.
Si nous prenons l’exemple du DAO de User, qui ne contient donc aucune méthode spécifique, sa création se limite aux deux fichiers suivants :
public interface IUserDao extends AbstractDao {
}
@Controller
public class UserDao extends AbstractJPADAOImpl implements IUserDao {
}
Lors de la définition des classes, nous spécifions le type d’objet du modèle sur lequel le DAO agit ainsi que la clé primaire afin de faire fonctionner la classe générique AbstractJPADAOImpl. De plus, on ajoute sur l’implémentation l’annotation @Controller qui servira ensuite à Spring à créer l’objet (cf. paragraphe sur Spring).
L’API Criteria
Maintenant que nous avons un endroit où écrire nos requêtes, voyons un peu comment les écrire.
Commençons avec un exemple simple, comment récupérer la liste des utilisateurs :
CriteriaQuery q = getBuilder().createQuery(User.class);
q.select(q.from(User.class));
return getEm().createQuery(q).getResultList();
À l’aide du CriteriaBuilder, nous construisons une CriteriaQuery qui nous renverra des objets User. Puis, nous effectuons un select depuis les objets de la classe User. Nous utilisons enfin l’EntityManager pour exécuter notre requête et nous renvoyer la liste du résultat.
Si nous reprenons l’exemple de tout à l’heure et voulons sélectionner uniquement les utilisateurs ayant le prénom Alice, il faudrait écrire :
CriteriaQuery q = getBuilder().createQuery(User.class);
Root user = q.from(User.class);
q.select(user);
q.where(q.equal(user.get(User_.firstname), "Alice"));
return getEm().createQuery(q).getResultList();
On voit ici l’utilisation du méta-modèle via la classe User_ qui permet de faire une restriction sur le prénom.
Prenons un exemple un peu plus complexe, la récupération de la liste des Item dans un Album :
public List getList(int photoAlbumId) {
CriteriaBuilder b = getBuilder();
//creating criteria
CriteriaQuery q = b.createQuery(AlbumItem.class);
Root item = q.from(AlbumItem.class);
q.select(item);
//joins to fetch
item.fetch(AlbumItem_.cameraOwner);
item.fetch(AlbumItem_.photoAlbum);
//adding restriction
q.where(b.equal(item.get(AlbumItem_.photoAlbum), photoAlbumId));
//ordering
q.orderBy(
b.asc(item.get(AlbumItem_.shootdate)),
b.asc(item.get(AlbumItem_.file))
);
return getEm().createQuery(q).getResultList();
}
L’utilisation de fetch permet de charger les attributs « externe » de la classe au moment de la requête SQL (à l’aide d’un JOIN) afin d’éviter que le parcours de la liste des items ne déclenche une requête à chaque affichage du prénom du preneur de la photo par exemple.
La restriction s’effectue ensuite sur un autre attribut « externe ». On remarque que même si le type de cet attribut est de la classe PhotoAlbum, JPA s’accommode tout à fait de la clé primaire associée à cet objet et on évite ainsi soit de le récupérer avant, soit de créer un « faux » objet qui n’aurait contenu que la clé.
Le reste permet d’ordonner les résultats avant d’effectuer la requête en base.
L’API Criteria n’est pas spécialement très facile à prendre en main, cependant je trouve que la vérification des types à la compilation est un véritable plus et évite de récupérer des erreurs douteuses à l’exécution suite a une requête mal écrite et mal testée. Je vous conseille de regarder les quelques exemples fourni dans le mini-projet et de ne pas hésiter à en piocher d’autres sur Internet.
L’intégration avec Spring et les tests
Maintenant que nous avons notre modèle en place et que nous pouvons y faire de requêtes, voyons comment utilisez ça dans le cadre de tests unitaires.
Afin de créer les différents DAO définis ci-avant, on utilise Spring. Celui-ci va se charger de faire les new pour créer les objets (en singleton) et va venir injecter la référence de ceux-ci dans nos tests (et à termes dans nos services). Les tests se trouvent dans le même package que les DAO (comme ça on peux tester les méthodes protected) mais dans le répertoire src/test/java et ne seront donc pas inclus dans le JAR final.
Voyons l’exemple du test de GroupDao :
public class GroupDaoTest extends AbstractDaoTest {
@Autowired
private IGroupDao groupDao;
@Test
public void testCount() {
assertEquals("Group Nb", 3, groupDao.count());
}
@Test
public void testGet() {
Group group = groupDao.get(2);
assertEquals("name", "Friends", group.getTitle());
}
@Test
public void testUsers() {
Group group = groupDao.get(1);
Set users = group.getUsers();
assertEquals("nbUsers", 2, users.size());
}
}
Afin de faire fonctionner Spring, la classe doit étendre de AbstractDaoTest. C’est cette classe qui s’occupe de démarrer le conteneur Spring qui créé les objets (avec @Component) et injecte les références.
Dans l’exemple, on voit que l’attribut groupDao de type IGroupDao a l’annotation @Autowired. Cette annotation Spring spécifie au conteneur que l’on souhaite se faire injecter la référence d’un objet implémentant l’interface IGroupDao. Étant donné qu’un seul objet déclaré en tant que tel existe (notre GroupDao avec son tag @Component), Spring va pouvoir y insérer la référence vers cet objet. Si aucun ou plusieurs objets avaient implémentés l’interface, Spring aurait levé une exception au démarrage de son conteneur.
Voyons comment fonctionne le démarrage du conteneur Spring dans la classe AbstractDaoTest :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/appDb.xml"})
@Transactional
public abstract class AbstractDaoTest extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
DataSource dataSrc;
private static boolean databaseLoaded = false;
@Before
public void beforeTest() {
setDataSource(dataSrc);
if (!databaseLoaded) {
super.executeSqlScript("classpath:test-data.sql", false);
databaseLoaded = true;
}
}
}
Plusieurs mécanismes rentrent en jeu ici :
- l’annotation @RunWith est une annotation JUnit qui prend en paramètre la classe qui va servir à démarrer les tests. On lui indique une classe du framework de test de Spring qui va s’occuper de démarrer le conteneur Spring.
- l’annotation @ContextConfiguration est une annotation Spring qui permet de spécifier le fichier de configuration Spring à utiliser pour démarrer le conteneur.
- l’annotation @Transactional est une annotation permettant de spécifier que l’appel de chaque méthode dans la classe de test démarre une transaction sur la base de données. Si une exception est levée dans la méthode, un rollback de la transaction est effectué, si tout se passe bien, un commit est fait.
- la classe hérite de AbstractTransactionalJUnit4SpringContextTests qui permet d’exposer une référence vers un object SimpleJdbcTemplate pouvant être utile si l’on souhaite passer des requêtes SQL en dur lors des tests (non utilisé ici). Elle sert aussi a gérer le mécanisme de transaction.
De plus, la classe comporte une méthode annotée avec @Before. Cette méthode est lancée avant chaque groupe de test et va insérer la référence de la datasource dans la classe parente. Si le jeu de donnée de test n’a pas encore été chargé, elle va le charger dans la base de données. On verra dans la configuration qu’on utilise ici une base de données embarquée en mémoire d’où l’obligation de recharger le jeu de test à chaque fois.
Enfin, voici le fameux fichier XML de configuration de Spring :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- H2 dataSource for testing environnement -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
<constructor-arg>
<bean class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=2" />
</bean>
</constructor-arg>
</bean>
<!-- provides a H2 console to look into the db if necessary -->
<!--
<bean id="org.h2.tools.Server-WebServer" class="org.h2.tools.Server"
factory-method="createWebServer" depends-on="dataSource"
init-method="start" lazy-init="false">
<constructor-arg value="-web,-webPort,11111" />
</bean>
-->
<!-- Loading JPA -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<prop key="hibernate.connection.release_mode">after_transaction</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<!-- Transaction Manager -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!-- Command list scanning -->
<context:component-scan base-package="org.alexthomazo.blog.model.dao"/>
</beans>
Trois beans sont créés et une configuration appellée :
- Le bean dataSource instancie la base de données embarquée en mémoire H2
- Le bean entityManagerFactory permet de créer l’EntityManager utilisé dans nos DAO et permettant d’effectuer les requêtes sur la base
- Le bean transactionManager permet de gérer les transactions de la base de données
- L’instruction component-scan indique à Spring quels packages scanner à la recherche d’annotation @Component pour savoir quels objets créer
Pour finir, il faut bien sûr importer toutes les dépendances qui vont bien dans le pom.xml :
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.164</version>
<scope>test</scope>
</dependency>
Il suffit maintenant de lancer notre test dans Eclipse avec un simple Run as > JUnit Test ou créer une configuration pour lancer les tests de tout le projet :
Si tout s’est bien passé, tout devrait être vert :
Conclusion
Ouf, voilà la fin. Finalement ça fait pas mal d’un coup, ceci dit une fois qu’on a compris comment tout ça s’imbrique, ça va un peu mieux.
N’hésitez pas à forker, à commenter, etc… 🙂
Pour finir, quelques liens utiles :
Commentaires récents