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 :
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 =
6670383237301287771
L;
@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 » :
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 ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
package
com.example.myproject.server.domain;
import
javax.annotation.Generated;
import
javax.persistence.metamodel.ListAttribute;
import
javax.persistence.metamodel.SingularAttribute;
import
javax.persistence.metamodel.StaticMetamodel;
@Generated
(
value =
"org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor"
)
@StaticMetamodel
(
Personalisation.class
)
public
abstract
class
Personalisation_ {
public
static
volatile
SingularAttribute<
Personalisation, Long>
id;
public
static
volatile
SingularAttribute<
Personalisation, String>
name;
public
static
volatile
ListAttribute<
Personalisation, Category>
categories;
public
static
volatile
SingularAttribute<
Personalisation, Long>
version;
}
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 » :
2.
3.
4.
5.
JPAQuery query =
new
JPAQuery (
entityManager);
QPersonalisation personalisation =
QPersonalisation.personalisation;
List<
Personalisation>
list =
query.from
(
personalisation)
.where
(
personalisation.name.eq
(
"toto"
)).list
(
personalisation);
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 ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
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;
/**
* QPersonalisation is a Querydsl query type for Personalisation
*/
@Generated
(
"com.mysema.query.codegen.EntitySerializer"
)
public
class
QPersonalisation extends
EntityPathBase<
Personalisation>
{
private
static
final
long
serialVersionUID =
787579482
;
public
static
final
QPersonalisation personalisation =
new
QPersonalisation
(
"personalisation"
);
public
final
ListPath<
Category, QCategory>
categories =
this
.<
Category, QCategory>
createList
(
"categories"
, Category.class
, QCategory.class
);
public
final
NumberPath<
Long>
id =
createNumber
(
"id"
, Long.class
);
public
final
StringPath name =
createString
(
"name"
);
public
final
NumberPath<
Long>
version =
createNumber
(
"version"
, Long.class
);
public
QPersonalisation
(
String variable) {
super
(
Personalisation.class
, forVariable
(
variable));
}
public
QPersonalisation
(
Path<
? extends
Personalisation>
entity) {
super
(
entity.getType
(
), entity.getMetadata
(
));
}
public
QPersonalisation
(
PathMetadata<
?>
metadata) {
super
(
Personalisation.class
, metadata);
}
}
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) :
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é.
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;
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 =
-
8384466127651010561
L;
@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;
}
}
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);
}
}
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);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
package
com.example.myproject.server.domain;
import
java.io.Serializable;
import
javax.persistence.Cacheable;
import
javax.persistence.Entity;
import
javax.persistence.GeneratedValue;
import
javax.persistence.Id;
import
javax.persistence.Version;
import
org.hibernate.annotations.Cache;
import
org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable
@Cache
(
usage =
CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public
class
Link implements
Serializable{
private
static
final
long
serialVersionUID =
-
8924619620063687148
L;
@Id
@GeneratedValue
private
Long id;
@Version
private
long
version;
private
String name;
}
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 :
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 » :
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.
<
?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).
2.
3.
4.
5.
6.
7.
8.
9.
10.
@Repository
public
class
DAOVisibleALExterieurDeLaCoucheDePersistance {
@PersistenceContext
private
EntityManager entityManager;
@Delegate
@Inject
private
DAOSpringData daoSpringData;
//mettez vos méthodes
}
2.
3.
public
interface
DAOSpringData extends
JpaRepository<
Personalisation,Long>{
// mettez vos query methods
}
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 :
<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.
<?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.
2.
3.
4.
@Entity
@Cacheable
@Cache
(
usage =
CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
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.
<?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.
<?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>
<?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>
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
<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.