Présentation d'outils à utiliser avec Hibernate

Présentation d'outils permettant d'accélérer le développement d'une couche de persistance reposant sur Hibernate

RonnyGuillaume

Le but de cet article est de faire découvrir des outils permettant d'accélérer le développement de la couche de persistance d'une application Java.

Cet article est destiné à celles et ceux qui ont déjà eu l'occasion de travailler avec des frameworks comme Hibernate, Spring et Maven.
Commentez

- Titre : Présentation d'outils à utiliser avec Hibernate

- Auteur : RonnyGuillaume

- Parution : 23 août 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ®  . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Type-safe criterias

- III. QueryDSL

- IV. Spring Data JPA

- V. EhCache (cache de second niveau)

- VI. JNDI

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Lorsque l'on doit persister ou récupérer des objets dans une base de données relationnelle (Oracle, MySQL, PostgreSQL, etc.), on est toujours confronté aux mêmes problèmes. Si le mapping entre les classes et les tables est assuré correctement par la plupart des ORM (Hibernate, EclipseLink, Entity Framework, etc.), il faut toujours écrire des tas de requêtes plus fastidieuses les unes que les autres pour récupérer des objets. Requêtes, qui lorsqu'elles sont invalides, n'empêchent pas votre application de compiler et de se lancer. Alors certes, si votre application est couverte à 100 % par des tests automatisés, vous serez informé assez tôt du problème, mais vous savez que, hélas, cette situation est très rare. De surcroît vous pouvez être confronté à des problèmes de performances, dues, entre autres, au fait que vous exécutez plusieurs fois les mêmes requêtes pour récupérer les mêmes données.

L'objet de cet article est de vous présenter des solutions, disponibles sur la plate-forme Java, à ces problèmes que l'on rencontre dans toutes les applications web (quelle que soit la technologie utilisée).

II - Type-safe criterias

L'immense majorité des développeurs Java connaissent Hibernate, ils connaissent les avantages principaux ainsi que les inconvénients inhérents à l'utilisation de cet ORM. Ce que bon nombre de développeurs ignorent, c'est l'existence des type-safe criterias. Cette fonctionnalité, qui pour rappel fait partie intégrante du standard JPA 2, est selon moi la plus grande avancée d'Hibernate depuis des années. Figurez-vous que grâce à ces criterias, on peut écrire des requêtes dont la correction est évaluée à la compilation. Je n'irai pas jusqu'à dire que dans le cas où toutes vos requêtes seraient écrites avec des type-safe criterias, si votre application compile, toutes vos requêtes sont valides. Néanmoins, l'utilisation de type-safe criterias limite considérablement les erreurs (syntaxe invalide, colonne inexistante, etc.). Cette caractéristique est particulièrement intéressante dans les cas de refactoring où vous êtes amené à renommer un champ d'une classe ou d'une table, ou pire, dans le cas où vous êtes amené à ajouter ou supprimer un champ à une classe ou une table ; une simple compilation de votre application mettra en lumière toutes les requêtes désormais invalides. Bien évidemment, tout ceci ne marche que si votre modèle objet est correctement mappé avec votre modèle relationnel.

Voici l'entité Personnalisation :

classe personalisation

package com.example.myproject.server.domain;

import java.io.Serializable;
import java.util.List;

import javax.persistence.Cacheable;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Version;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Personalisation implements Serializable{

private static final long serialVersionUID = 6670383237301287771L;
@Id
@GeneratedValue
private Long id;
@Version
private long version;

private String name;
@OneToMany(cascade=CascadeType.ALL)
private List<Category> categories;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Category> getCategories() {
return categories;
}
public void setCategories(List<Category> categories) {
this.categories = categories;
}
}

Dans l'exemple suivant on retourne tous les objets « Personne » dont le champ « name » est égal à la chaîne de caractères « toto » :

Type safe Criteria

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Personalisation> query =cb.createQuery(Personalisation.class);
Root<Personalisation> from = query.from(Personalisation.class);
query.where(cb.like(from.get(Personalisation_.name), cb.literal("toto")));
TypedQuery<Personalisation> tq = entityManager.createQuery(query);
return    tq.getResultList();

Vous vous demandez sûrement d'où vient la classe « Personalisation_ ». Elle a été générée par Hibernate (via un plugin Maven, cf. le fichier pom.xml présent plus loin dans l'article) à partir de la classe « Personnalisation ». On appelle ces classes générées qui ont un « _ » pour suffixe, des métamodèles. C'est grâce à elles que les requêtes par criterias sont « type-safe ».

Classe Personalisation_ générée

1. package com.example.myproject.server.domain;

3. import javax.annotation.Generated;

4. import javax.persistence.metamodel.ListAttribute;

5. import javax.persistence.metamodel.SingularAttribute;

6. import javax.persistence.metamodel.StaticMetamodel;

8. @Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")

9. @StaticMetamodel(Personalisation.class)

10. public abstract class Personalisation_ {

12. public static volatile SingularAttribute<Personalisation, Long> id;

13. public static volatile SingularAttribute<Personalisation, String> name;

14. public static volatile ListAttribute<Personalisation, Category> categories;

15. public static volatile SingularAttribute<Personalisation, Long> version;

17. }

Pour que les requêtes effectuées avec les type-safe criterias soient valides, il faut d'une part que votre modèle objet soit correctement mappé à la base de données, mais d'autre part que chaque entité de votre modèle soit toujours en phase avec son métamodèle correspondant. Il est donc nécessaire à chaque fois que vous modifiez vos entités de régénérer vos métamodèles (via un plugin maven par exemple).

Sachez que vous pouvez définir vos propres criterias. Mettons que vous ayez besoin de faire une recherche insensible à la casse et à l'accentuation sur un champ de type String. L'insensibilité à l'accentuation n'existant pas dans JPA 2, vous vous dites qu'il va peut-être falloir écrire votre requête en SQL. Eh bien, pas forcément, vous pouvez très bien vous inspirer de l'exemple suivant : http://baldercm.blogspot.fr/2012/04/howto-invoke-database-functions-in-jpa.html.

Alors certes, les type-safe criterias ne vous permettront pas de vous assurer que vos requêtes sont en phase avec les exigences clients, en d'autres termes qu'elles retournent bien ce qu'elles doivent retourner au niveau fonctionnel, mais vous éviterez au moins les plantages à l'exécution.

Problème, comme vous avez pu le constater, ces requêtes, bien que relativement sûres, ne sont pas très lisibles. C'est là que QueryDSL intervient.

III - QueryDSL

Ce framework (http://www.querydsl.com/) est en fait une API permettant d'effectuer des criterias avec un niveau de sûreté proche de celles que l'on trouve dans JPA 2, mais avec une lisibilité et une concision accrue.

Dans l'exemple suivant on retourne encore tous les objets « Personne » dont le champ « name » est égal à la chaîne de caractères « toto » :

QueryDSL criteria

1. JPAQuery query = new JPAQuery (entityManager);

2. QPersonalisation personalisation =QPersonalisation.personalisation;

3. List<Personalisation> list = query.from(personalisation)

4. .where(personalisation.name.eq("toto")).list(personalisation);

5. return    list;

La classe QPersonnalisation présente dans la requête est un métamodèle QueryDSL que l'on peut générer via un plugin maven (cf. le fichier pom.xml présent plus loin dans l'article). Les métamodèles QueryDSL sont préfixés par la lettre « Q ».

Classe QPersonnalisation générée par le plugin maven de QueryDSL

1. package com.example.myproject.server.domain;

3. import static com.mysema.query.types.PathMetadataFactory.*;

5. import com.mysema.query.types.*;

6. import com.mysema.query.types.path.*;

8. import javax.annotation.Generated;

11. /**

12. * QPersonalisation is a Querydsl query type for Personalisation

13. */

14. @Generated("com.mysema.query.codegen.EntitySerializer")

15. public class QPersonalisation extends EntityPathBase<Personalisation> {

17. private static final long serialVersionUID = 787579482;

19. public static final QPersonalisation personalisation = new QPersonalisation("personalisation");

21. public final ListPath<Category, QCategory> categories = this.<Category, QCategory>createList("categories", Category.class, QCategory.class);

23. public final NumberPath<Long> id = createNumber("id", Long.class);

25. public final StringPath name = createString("name");

27. public final NumberPath<Long> version = createNumber("version", Long.class);

29. public QPersonalisation(String variable) {

30. super(Personalisation.class, forVariable(variable));

31. }

33. public QPersonalisation(Path<? extends Personalisation> entity) {

34. super(entity.getType(), entity.getMetadata());

35. }

37. public QPersonalisation(PathMetadata<?> metadata) {

38. super(Personalisation.class, metadata);

39. }

41. }

Vous allez sûrement me dire que lorsque l'on récupère des données, elles ne correspondent pas forcément à un seul et même objet (ou une seule table) de votre modèle, mais assez souvent à un ensemble de données piochées sur plusieurs objets (ou tables). Les projections (qui existent aussi dans l'API Hibernate) permettent de répondre à cette problématique.

Voici la classe CategoryDTO, elle contient des données issues de plusieurs classes (Category et Personnalisation) :

Classe CategoryDTO

package com.example.myproject.server.dto;

import com.mysema.query.annotations.QueryProjection;

public class CategoryDTO {

private String categoryName;
private Long  categoryId;
private String personalisationName;

@QueryProjection
public CategoryDTO(String categoryName, Long categoryId,
String personalisationName) {

this.categoryName = categoryName;
this.categoryId = categoryId;
this.personalisationName = personalisationName;
}

public String getCategoryName() {
return categoryName;
}

public Long getCategoryId() {
return categoryId;
}

public String getPersonalisationName() {
return personalisationName;
}
}

Dans l'exemple suivant on initialise des instances de la classe « CategoryDTO » avec le nom et l'id des objets de type « Category », ainsi qu'avec le nom de l'objet « Personnalisation » qui leur est rattaché.

Requête avec projection

JPAQuery query = new JPAQuery (entityManager);
QCategory category =QCategory.category;
QPersonalisation personalisation =QPersonalisation.personalisation;
List<CategoryDTO> list = query.from(personalisation).innerJoin(personalisation.categories, category)
.list(ConstructorExpression.create(CategoryDTO.class, category.name,category.id,personalisation.name));
return    list;

classe Category

package com.example.myproject.server.domain;

import java.io.Serializable;
import java.util.List;

import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Version;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Category implements Serializable{

private static final long serialVersionUID = -8384466127651010561L;
@Id
@GeneratedValue
private Long id;
@Version
private long version;
private String name;
@OneToMany
private List<Link> links;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Classe QCategoryDTO généré par le plugin maven de QueryDSL

package com.example.myproject.server.dto;

import com.mysema.query.types.*;
import com.mysema.query.types.expr.*;

/**
* com.example.myproject.server.dto.QCategoryDTO is a Querydsl Projection type for CategoryDTO
*/
public class QCategoryDTO extends ConstructorExpression<CategoryDTO> {

private static final long serialVersionUID = 1886801561;

public QCategoryDTO(StringExpression categoryName, NumberExpression<Long> categoryId, StringExpression personalisationName) {
super(CategoryDTO.class, new Class[]{String.class, long.class, String.class}, categoryName, categoryId, personalisationName);
}

}

classe QCategory générée par le plugin maven de QueryDSL

package com.example.myproject.server.domain;

import static com.mysema.query.types.PathMetadataFactory.*;

import com.mysema.query.types.*;
import com.mysema.query.types.path.*;

import javax.annotation.Generated;

/**
* QCategory is a Querydsl query type for Category
*/
@Generated("com.mysema.query.codegen.EntitySerializer")
public class QCategory extends EntityPathBase<Category> {

private static final long serialVersionUID = -1477290161;

public static final QCategory category = new QCategory("category");

public final NumberPath<Long> id = createNumber("id", Long.class);

public final ListPath<Link, QLink> links = this.<Link, QLink>createList("links", Link.class, QLink.class);

public final StringPath name = createString("name");

public final NumberPath<Long> version = createNumber("version", Long.class);

public QCategory(String variable) {
super(Category.class, forVariable(variable));
}

public QCategory(Path<? extends Category> entity) {
super(entity.getType(), entity.getMetadata());
}

public QCategory(PathMetadata<?> metadata) {
super(Category.class, metadata);
}

}

Classe Link

1. package com.example.myproject.server.domain;

3. import java.io.Serializable;

5. import javax.persistence.Cacheable;

6. import javax.persistence.Entity;

7. import javax.persistence.GeneratedValue;

8. import javax.persistence.Id;

9. import javax.persistence.Version;

11. import org.hibernate.annotations.Cache;

12. import org.hibernate.annotations.CacheConcurrencyStrategy;

14. @Entity

15. @Cacheable

16. @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)

17. public class Link implements Serializable{

19. private static final long serialVersionUID = -8924619620063687148L;

20. @Id

21. @GeneratedValue

22. private Long id;

23. @Version

24. private long version;

25. private String name;

27. }

Notez que l'utilisation des projections vous évite de convertir à la main vos objets du domaine en DTO.

Pour des raisons de lisibilité du code, je vous conseille de privilégier autant que possible les criteria QueryDSL aux type-safe criterias de la norme JPA 2.

IV - Spring Data JPA

Tout ça est bien beau, mais si on a envie d'être encore plus productif ? Si la sûreté du typage ne nous suffit plus ? Si le fait même d'écrire une requête, aussi simple soit-elle, nous semble fastidieux ? C'est là que Spring Data JPA (http://www.springsource.org/spring-data/jpa) entre en jeu.

Spring Data JPA est un framework qui a besoin du cadriciel Spring pour fonctionner, ce framework vous apportera non seulement une classe abstraite générique implémentant des méthodes de persistance basiques (save, delete, findOne, count, findAll, etc.), mais aussi les « query methods ».

Mais que sont les « query methods » ? À quoi ça sert ? Comment cela fonctionne ? Comment les utiliser ?

À vrai dire c'est un peu magique, vous créez une interface qui hérite de « JpaRepository », vous renseignez le type de l'objet que vous persistez puis vous précisez le type de sa clé primaire. Jusqu'ici vous n'avez quasiment rien fait, et pourtant vous disposez d'un bean Spring dont le nom est le même que celui de votre interface (la première lettre du nom de l'interface étant en minuscule) qui implémente des méthodes de persistance assez basiques (save, delete, findOne, count, findAll, etc.) vous permettant de faire du CRUD, du tri, de la pagination, etc. Là où ça devient effarant, c'est qu'en déclarant une méthode dans cette interface (qui n'est jamais implémentée par quelque classe que ce soit) en respectant un certain formalisme (http://static.springsource.org/spring-data/data-jpa/docs/current/reference/html/#jpa.query-methods.query-creation), vous pourrez utilisez cette méthode sans avoir à écrire la moindre requête (que ce soit en criteria, JPQL, SQL ou autre). C'est comme si Spring devinait la requête à exécuter juste en fonction du nom de la méthode. Voici un exemple :

Interface Spring Data

package com.example.myproject.server.daos.springdata;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.myproject.server.domain.Personalisation;

public interface PersonalisationRepository extends JpaRepository<Personalisation,Long>{

List<Personalisation> findByname(String lastname);
}

Dans l'exemple suivant on cherche encore tous les objets « Personnalisation » dont le champ « name » est égal à la chaîne de caractères « toto » :

Comment utiliser l'interface Spring Data

package com.example.myproject.server.services;

import java.util.List;

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import com.example.myproject.server.daos.springdata.PersonalisationRepository;
import com.example.myproject.server.domain.Personalisation;

@Service
public class ClasseDeService {

@Inject
private PersonalisationRepository personalisationDAO;

public void methodeQuelconque(){
List<Personalisation> persos=personalisationDAO.findByname("toto");
System.out.println(persos.get(0).getName());
}
}

Au niveau de la configuration Spring, rien de bien particulier hormis la présence de la balise <jpa:repositories/>, indispensable au fonctionnement de Spring Data JPA.

fichier de configuration spring « services-servlet.xml » présent dans le répertoire WEB-INF

<?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" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa  http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop   http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">

<context:component-scan base-package="com.example.myproject.server" />

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="MonTest" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
</bean>

<aop:aspectj-autoproxy proxy-target-class="true" />
<aop:config proxy-target-class="true">
<!-- On rend toutes les methodes des classes de service transactionnelles -->
<aop:pointcut id="serviceMethods"
expression="execution(* com.example.myproject.server.services.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods" />
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- Spring Data analyse uniquement les interfaces du package  com.example.myproject.server.daos.springdata -->
<jpa:repositories base-package="com.example.myproject.server.daos.springdata" />
</beans>

Le gain de productivité est évident car pour toutes les requêtes simples, une simple déclaration de méthode suffit. De plus le formalisme imposé par Spring (pour que la magie fonctionne) vous oblige à avoir des noms de méthodes (à rallonge) claires qui ne laissent aucune ambiguïté sur ce qui est retourné par votre fonction. À noter que si votre nom de « query method » fait référence à un champ qui n'existe pas sur l'entité, Spring plantera au démarrage du serveur (et non à l'exécution) avec un message d'erreur vous expliquant l'erreur commise.

Cette utilisation de Spring Data pose une question architecturale : comment utiliser des type-safe criterias et des « quey method » dans un même « dao » ? Eh bien il y aura une interface Spring Data, comme celle que je vous ai présentée plus tôt dans l'article, qui contiendra toutes vos « query method » et une classe concrète qui contiendra vos requêtes avec criterias et de manière plus générale, tout ce que vous ne pouvez pas mettre dans l'interface Spring Data. Vous vous retrouvez donc avec deux « dao », un pour exécuter les requêtes Spring Data et un autre pour exécuter les autres requêtes (criteria, JPQL, SQL, etc.). C'est moche… La solution que je vous propose, consiste à ce que la classe concrète soit un bean Spring, qui soit le seul « dao » visible en dehors de la couche de persistance. Cette classe implémentera toutes les méthodes de l'interface Spring Data en les déléguant au bean Spring correspondant (en utilisant l'annotation @Delegate du framework Lombok - http://projectlombok.org/features/Delegate.html - par exemple).

Dao exposé à l'exterieur de la couche de persistance

1. @Repository

2. public class DAOVisibleALExterieurDeLaCoucheDePersistance {

3. @PersistenceContext

4. private EntityManager entityManager;

6. @Delegate

7. @Inject

8. private DAOSpringData daoSpringData;

9. //mettez vos méthodes

10. }

DAO Spring Data

1. public interface DAOSpringData extends JpaRepository<Personalisation,Long>{

2. // mettez vos query methods

3. }

Il est désormais temps de faire un point. Les trois méthodes de requêtage que je vous ai présentées vous feront soit gagner en productivité, soit en qualité (en vous signalant au plus tôt les problèmes dans vos requêtes), voire les deux à la fois. Mais entendons-nous bien, si la plupart des requêtes peuvent (et devraient) être écrites avec une de ces trois manières de faire, il existe bien évidement des requêtes qui ne peuvent être traduites de manière pertinente en criterias ou en « query method ». Il est bien évident que de telles requêtes doivent êtres écrites en JPQL ou en SQL. Ces outils, mêmes combinés, n'ont pas la prétention de se substituer totalement au SQL. Ils ont juste été conçus pour accélérer et sécuriser le développement d'une couche de persistance.

V - EhCache (cache de second niveau)

Si vous connaissez un peu Hibernate, vous savez qu'il est possible de configurer un cache de second niveau, et ce, bien évidemment dans le but d'améliorer les performances.
En dehors de cas très particuliers (et donc rarissimes), tout projet utilisant Hibernate devrait utiliser un cache de second niveau, bonne pratique qui n'est, à mes yeux, pas assez appliquée.

Mais à quoi sert le cache de second niveau au juste ?

Hibernate, comme vous le savez, persiste et récupère des objets dans des bases de données, et ce cache, sert à sauvegarder ces objets en mémoire, afin d'économiser de coûteux accès en base. Prenons un exemple, si vous avez un site de e-commerce, que vous vendez des articles, vous aurez un certain nombre d'utilisateurs qui viendront consulter ces articles (dans le but de les acheter). Avec le cache de second niveau d'Hibernate, quand un utilisateur consultera un article (qui n'a jusqu'à présent jamais été consulté), une requête sera effectuée en base et l'objet article correspondant sera chargé une première fois dans le cache. Quand un autre utilisateur voudra consulter le même article, aucune requête ne sera effectuée, Hibernate récupérera l'objet correspondant directement dans le cache de second niveau. Le gain en performance est évident. Bien évidemment, on peut paramétrer ce cache afin de limiter la quantité de mémoire qui lui est alloué, gérer les problèmes d'accès concurrentiels à la base de données, etc.

Différentes implémentations de cache de second niveau pour Hibernate existent (HashTable, EhCache, OsCache, SwarmCache, Infinispan, etc.). Celle qui est selon moi, la mieux documentée et la plus adaptée aux projets informatiques courants (les applications web) est EhCache (http://ehcache.org/), conçu par Terracotta.
Si vous consultez la documentation officielle (probablement pas à jour), vous constaterez avec effroi qu'elle est, en ce qui concerne le « hibernate.cache.region.factory_class », non applicable aux dernières versions d'Hibernate. C'est pour cela que je vous conseille d'utiliser celui renseigné dans le fichier « persistence.xml » présent dans cet article, en lieu et place de celui mentionné dans la documentation officielle d'EhCache. Hormis ce petit souci, cette implémentation de cache de second niveau fonctionne à merveille avec les dernières versions d'Hibernate.

Un fichier nommé « ehcache.xml » est indispensable au fonctionnement d'EhCache, ce fichier, à mettre à la racine de votre classpath, centralise la quasi-totalité de la configuration du cache de second niveau. Voici un exemple de contenu de ce fichier :

fichier de configuration ehcache

<ehcache maxBytesLocalHeap="256M">
<diskStore path="java.io.tmpdir"/>
<defaultCache   eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="240"
overflowToDisk="true"
diskExpiryThreadIntervalSeconds="120" />
</ehcache>

Pour plus d'informations sur la configuration du fichier « ehcache.xml », je vous invite à consulter la documentation officielle.

fichier persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.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_2_0.xsd">
<persistence-unit name="MonTest" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:/comp/env/jdbc/postgres</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="hibernate.order_updates" value="true" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.generate_statistics" value="true" />
<property name="hibernate.use_sql_comments" value="true" />
<property name="hibernate.connection.autocommit" value="false" />
<!-- On dit à hibernate que l'on utilise un cache de second niveau -->
<property name="hibernate.cache.use_second_level_cache" value="true" />
<property name="hibernate.cache.use_query_cache" value="true" />

<!-- On dit à hibernate que l'on utilise EhCache -->
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory" />
</properties>
</persistence-unit>
</persistence>

N'oubliez pas l'annotation @Cacheable à mettre sur vos entités afin qu'elles soient prises en compte par le cache. Par défaut, le cache de second niveau ne s'occupe pas des entités qui n'ont pas reçu cette annotation.

Classe Personnalisation gérée par le cache de second niveau

1. @Entity

2. @Cacheable

3. @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)

4. public class Personalisation implements Serializable{

L'annotation @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) sert à dire qu'une même instance de la classe Personnalisation ne sera pas modifiée par deux transactions simultanément, mais qu'elle peut être accédée en lecture en même temps par deux transactions.

VI - JNDI

On ne le dira jamais assez, externalisez au maximum votre configuration. Beaucoup de développeurs pensent le faire en la mettant dans des fichiers XML ou dans des fichiers properties. Le problème avec cette façon de procéder, c'est que certes, elle évite une recompilation (qui en Java n'est pas si longue que ça), mais elle oblige de toute façon de refaire le WAR dans lequel se trouvent ces fichiers de configuration. Une solution pourrait être de s'organiser pour que ces fichiers ne soient pas inclus dans le WAR mais sur l'environnement d'exécution de l'application, et toujours au même endroit. Par exemple vous pouvez configurer vos serveurs de recettes, de productions et de développements pour que le fichier properties qui contient toutes les informations permettant de se connecter à la base de données soit à la racine de chaque serveur (« /conf.properties »).

Une autre façon de faire est d'utiliser JNDI. Je ne vais pas vous définir ce qu'est JNDI mais au moins vous dire comment l'utiliser pour stocker les informations de connexion à la base de données. L'intérêt de réellement externaliser sa configuration, est que si votre WAR ne contient aucune information spécifique à tel ou tel environnement d'exécution, vous pouvez utiliser le même partout ! En fait l'idée ici, c'est de ne pas configurer l'application pour qu'elle puisse fonctionner sur un environnement d'exécution bien précis, mais de configurer cet environnement pour que l'application puisse y fonctionner.

Dans cet exemple, j'ai choisi d'utiliser la base de données PostgreSQL 9 et le serveur Apache Tomcat 7. J'ai configuré toutes les informations de connexion dans le fichier « context.xml » de Tomcat. Elles sont donc utilisables par tous les WAR déployés sur ce serveur. Il est néanmoins possible de définir une configuration spécifique à chaque application. Si vous n'utilisez pas Tomcat, sachez que JNDI est utilisable avec d'autres serveurs (JBoss, Glassfish, etc.), chaque serveur Java disposant de sa propre configuration JNDI. Je vous invite à lire la documentation correspondante.

Fichier Context.xml du serveur Tomcat

<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License.  You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--><!-- The contents of this file will be loaded for each web application -->
<Context>

<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>

<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->

<!-- Uncomment this to enable Comet connection tacking (provides events
on session expiration as well as webapp lifecycle) -->
<!--
<Valve className="org.apache.catalina.valves.CometConnectionManagerValve" />
-->
<Resource name="jdbc/postgres" auth="Container"
type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/montest"
username="postgres" password="postgres" maxActive="20" maxIdle="10" maxWait="-1"/>
</Context>

Grâce à cette configuration on peut accéder à la base de données via l'URL JNDI suivante : « java:/comp/env/jdbc/postgres ». Notez qu'on retrouve cette URL entre les balises <jta-data-source> du fichier « persistence.xml » présenté plus tôt dans l'article.

VII - Conclusion

Voici toute la configuration nécessaire pour mettre sur pied un projet Java avec Hibernate, Maven, Spring Data, QueryDSL et EhCache. Ces fichiers de configuration vous feront gagner à coup sûr un temps précieux lors du démarrage de vos développements. Je vous conseille néanmoins de consulter préalablement la documentation de chaque framework. Pour rappel la base de données utilisée est PostgreSQL 9, le serveur utilisé est Apache Tomcat 7.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/services-servlet.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--...-->

<!-- Default page to serve -->
<welcome-file-list>
<welcome-file>MonTest.html</welcome-file>
</welcome-file-list>

</web-app>

Fichier aop.xml à mettre dans le répertoire META-INF qui est à la racine du classpath

<?xml version="1.0" encoding="UTF-8"?>
<aspectj><weaver options="-verbose -showWeaveInfo -debug">
<include within="com.example.*"/>
<exclude within="org.hibernate.*"/>
<exclude within="javax.*"/>
</weaver></aspectj>

Fichier de configuration log4j.properties qui affiche des informations sur le cache de second niveau

log4j.rootLogger=off,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
log4j.logger.org.hibernate=off
log4j.logger.org.hibernate.cache=TRACE

fichier de configuration maven pom.xml

<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>MonTest</groupId>
<artifactId>MonTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901-1.jdbc4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.4.Final</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>4.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>2.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.11.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.1.2</version>
</dependency>
</dependencies>
<build>
<defaultGoal>package</defaultGoal>
<sourceDirectory>src</sourceDirectory>
<outputDirectory>war/WEB-INF/classes</outputDirectory>
<resources>
<resource>
<directory>resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- on nettoie les fichiers générés pour éviter les warnings et autres erreurs -->
<filesets>
<fileset>
<directory>generated-src</directory>
<includes>
<include>**/*</include>
</includes>
</fileset>
<fileset>
<directory>war/WEB-INF/lib</directory>
<includes>
<include>*.jar</include>
</includes>
</fileset>
</filesets>
</configuration>
<executions>
<execution>
<id>cleanThat</id>
<phase>process-resources</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<excludeDefaultDirectories>true</excludeDefaultDirectories>
<filesets>
<fileset>
<directory>trash</directory>
<includes>
<include>**/*</include>
</includes>
</fileset>
</filesets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<!-- on choisi de mettre les fichiers générés dans un répertoire a part -->
<generatedSourcesDirectory>generated-src</generatedSourcesDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>2.0.5</version>
<executions>
<!-- On génére les classes de meta-modèle afin de faire des requêtes criterias. -->
<execution>
<id>jpametamodel</id>
<goals>
<goal>process</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<!-- ce plugin est assez idiot, cet outputDirectory vers un répertoire inutile est nécéssaire pour ne pas
avoir quelques déconvenues -->
<outputDirectory>trash</outputDirectory>
<processors>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</processors>
</configuration>
</execution>
<execution>
<id>querydslmetamodel</id>
<goals>
<goal>process</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<outputDirectory>generated-src</outputDirectory>
<processors>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</processors>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.2.0.Final</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<webXml>war/WEB-INF/web.xml</webXml>
<webappDirectory>war</webappDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>hibernate3-maven-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<!-- génération du schéma de la base de données à partir des classes -->
<goal>hbm2ddl</goal>
</goals>
</execution>
</executions>
<configuration>
<components>
<component>
<name>hbm2ddl</name>
<implementation>jpaconfiguration</implementation>
</component>
</components>
<componentProperties>
<persistenceunit>MonTest</persistenceunit>
<outputfilename>schema.ddl</outputfilename>
<drop>true</drop>
<create>true</create>
<export>false</export>
<format>true</format>
</componentProperties>
</configuration>
</plugin>

</plugins>
</build>
<repositories>
<repository>
<id>mysema</id>
<url>http://source.mysema.com/maven2/releases</url>
</repository>

<repository>
<id>springsource-repo</id>
<name>SpringSource Repository</name>
<url>http://repo.springsource.org/release</url>
</repository>
<repository>
<id>terracotta-releases</id>
<url>http://www.terracotta.org/download/reflector/releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

J'espère que cet article vous aidera à améliorer la productivité et la qualité de vos développements Java ainsi que les performances de vos applications. Sachez toutefois que l'optimisation de la couche de persistance d'une application Java ne se fait pas uniquement dans la partie Java. C'est bien de configurer correctement son ORM, de faire de belles requêtes SQL ultraoptimisées, mais c'est bien aussi d'optimiser directement le fonctionnement de la base de données, et pour ça je vous renvoie à la documentation de votre base de données favorite.

VIII - Remerciements

Je tiens à remercier Keulkeul, Nemek et azerr pour leur relecture technique, ainsi que f-leb pour sa relecture orthographique.