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
CacherSélectionnez
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
CacherSélectionnez
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
CacherSélectionnez
  1. package com.example.myproject.server.domain; 
  2.  
  3. import javax.annotation.Generated; 
  4. import javax.persistence.metamodel.ListAttribute; 
  5. import javax.persistence.metamodel.SingularAttribute; 
  6. import javax.persistence.metamodel.StaticMetamodel; 
  7.  
  8. @Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") 
  9. @StaticMetamodel(Personalisation.class) 
  10. public abstract class Personalisation_ { 
  11.  
  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; 
  16.  
  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
Sélectionnez
  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
Sélectionnez
  1. package com.example.myproject.server.domain; 
  2.  
  3. import static com.mysema.query.types.PathMetadataFactory.*; 
  4.  
  5. import com.mysema.query.types.*; 
  6. import com.mysema.query.types.path.*; 
  7.  
  8. import javax.annotation.Generated; 
  9.  
  10.  
  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> { 
  16.  
  17.     private static final long serialVersionUID = 787579482; 
  18.  
  19.     public static final QPersonalisation personalisation = new QPersonalisation("personalisation"); 
  20.  
  21.     public final ListPath<Category, QCategory> categories = this.<Category, QCategory>createList("categories", Category.class, QCategory.class); 
  22.  
  23.     public final NumberPath<Long> id = createNumber("id", Long.class); 
  24.  
  25.     public final StringPath name = createString("name"); 
  26.  
  27.     public final NumberPath<Long> version = createNumber("version", Long.class); 
  28.  
  29.     public QPersonalisation(String variable) { 
  30.         super(Personalisation.class, forVariable(variable)); 
  31.     } 
  32.  
  33.     public QPersonalisation(Path<? extends Personalisation> entity) { 
  34.         super(entity.getType(), entity.getMetadata()); 
  35.     } 
  36.  
  37.     public QPersonalisation(PathMetadata<?> metadata) { 
  38.         super(Personalisation.class, metadata); 
  39.     } 
  40.  
  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
Sélectionnez
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
CacherSélectionnez
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
CacherSélectionnez
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
CacherSélectionnez
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
Sélectionnez
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
Sélectionnez
  1. package com.example.myproject.server.domain; 
  2.  
  3. import java.io.Serializable; 
  4.  
  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; 
  10.  
  11. import org.hibernate.annotations.Cache; 
  12. import org.hibernate.annotations.CacheConcurrencyStrategy; 
  13.  
  14. @Entity 
  15. @Cacheable 
  16. @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 
  17. public class Link implements Serializable{ 
  18.  
  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; 
  26.  
  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
CacherSélectionnez
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
CacherSélectionnez
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
CacherSélectionnez
<?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
Sélectionnez
  1.  
  2. @Repository 
  3. public class DAOVisibleALExterieurDeLaCoucheDePersistance { 
  4.     @PersistenceContext 
  5.     private EntityManager entityManager; 
  6.  
  7.     @Delegate 
  8.     @Inject 
  9.     private DAOSpringData daoSpringData; 
  10. //mettez vos méthodes 
  11. } 
DAO Spring Data
Sélectionnez
  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
CacherSélectionnez
<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
CacherSélectionnez
<?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
Sélectionnez
  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
CacherSélectionnez
<?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
CacherSélectionnez
<?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
CacherSélectionnez
<?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
CacherSélectionnez
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
CacherSélectionnez
<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.