JPA pour uniformiser la persistance Java ?

#java#jee#ORM

Avec l’arrivée des EJB3, Sun a mis en place une nouvelle API, « Java Persistence API » également connue sous le sigle JPA. JPA est une spécification qui définit un framework de persistance, introduite dans la JSR-220 (nom officiel de la spécification EJB3.0).Les frameworks de persistance, également connus sous le nom d’ORM (Object Relationnal Mapping), ont pour objectif de fournir un mapping objet/relationnel entre les SGBDR et les applications Java. Les plus répandus sur le marché sont Hibernate ou encore TopLink. JPA a tenu compte de l’ensemble de ces frameworks pour standardiser l’univers de la persistance en Java.

Comprendre le fonctionnement

Dans la plupart des architectures applicatives, une couche d’accès aux données est modélisée. Il s’agit de la couche communément appelée DAO (Data Access Object).

Dans une configuration classique, cette couche interroge directement le SGBDR à l’aide de son pilote JDBC.

Dans une configuration utilisant la spécification JPA couplée à un ORM l’implémentant, le cheminement des informations entre la DAO et le SGBDR se trouve modifié. En effet, la couche DAO dialogue, à travers la spécification JPA, avec l’ORM qui fait ensuite le lien avec le SGBDR. L’ORM se charge du pont entre le monde relationnel de la base de données et les objets Java de l’application. L’ensemble des objets gérés par la couche JPA à travers ce pont, forme « le contexte de persistance ».

Au cours du cycle de vie du contexte de persistance, l’objet peut prendre différents états :

  • « persisté » : lorsque l’objet est géré par le contexte de persistance.
  • « non persisté » : en opposition à « persisté », signifie qu’un objet n’est pas géré par le contexte de persistance.
  • « détaché » : dans le cas d’un objet déjà persisté mais dont le contexte de persistance a été fermé.

Pour gérer ce cycle de vie, la couche JPA fournit l’interface « EntityManager ».

Voici une liste non exhaustive des fonctions disponibles à travers cette interface et leurs descriptions :

D’autres méthodes sont également présentes afin d’exécuter des requêtes spécifiques sur la base. Ces requêtes peuvent être écrites en JPQL (Java Persistence Query Language) ou en SQL natif. Elles peuvent également être centralisées dans des fichiers de configuration externes.

Le langage JPQL, à l’inverse du SQL, exécute ses requêtes sur des objets et non sur des tables. Les similitudes entre les deux langages sont nombreuses. Cependant, le fait que les requêtes JPQL ne soient plus directement liées au SGBDR offre une plus grande portabilité.

Exemple de requête sur un objet Client :

Le pont objet / relationnel est configurable à partir de fichiers XML ou plus récemment (depuis le JDK 1.5) à l’aide d’annotations Java directement dans les classes.

Les annotations peuvent être placées au dessus de la déclaration des champs (cf. exemple ci-dessous) ou au dessus des getters. C’est la position de l’annotation @Id qui fixe celle des autres. Cependant, ce placement n’est pas un simple choix esthétique. En effet, si les annotations se trouvent au niveau des getters des champs alors JPA accèdera à ces derniers par le biais des méthodes get et set pour y lire et y écrire les informations. Dans le cas contraire, l’accès se fera directement par le champ.

Cet exemple de code met en œuvre quelques annotations incontournables :

Description de ces annotations :

  • @Entity est indispensable. Elle signifie que la classe doit être gérée par la couche de persistance.
  • @Table désigne la table utilisée en base pour représenter cet objet. Dans notre cas, cette annotation est facultative car par défaut la table prend le nom de la classe.
  • @Id indique que le champ est la clé primaire de la table.
  • @GeneratedValue avec son argument « strategy » apporte des informations sur la manière de gérer la clé primaire. Ici, elle est gérée de manière automatique par le SGBDR.
  • @Column crée le lien entre la représentation du champ en base et sa déclaration dans le code. Plusieurs arguments, tous optionnels, sont paramétrables à travers cette annotation :

Pour des relations plus complexes, d’autres annotations sont nécessaires. Dans l’exemple, l’annotation « @ManyToOne » est utilisée pour modéliser la relation entre la table « Client » et la table « Commande » (un client peut avoir plusieurs commandes « 1-N »). C’est la relation principale. « @JoinColum » indique le nom de la colonne qui contiendra la contrainte d’intégrité vers la table « Client ». Dans la classe « Client », l’annotation « @OneToMany » représente la relation inverse (« mappedBy »). Elle est facultative contrairement à la relation principale. Cependant, sont utilisation dans ce cas permet de récupérer la liste des commandes du client sans passer par une requête JPQL spécifique.

Deux autres annotations viennent compléter la modélisation de contraintes entre les objets :

  • @OneToOne : Dans le cas d’une relation « 1-1 ».
  • @ManyToMany : Pour les relations « N-N ». (Elle sera modélisée en base de données par une table de jointure.)

Dans l’exemple ci-dessus, deux attributs sont utilisés dans l’annotation « @OneToMany », « Cascade » et « Fetch ». La valeur « CascadeType.ALL » indique à l’ORM que toutes les opérations (PERSIST, MERGE, REMOVE, REFRESH) effectuées sur l’objet Client sont implicitement répercutées sur ses commandes. Ainsi, lors de la suppression d’un client, l’ORM supprimera en cascade toutes les commandes rattachées à ce dernier. Cet attribut « Cascade » doit donc être utilisé avec parcimonie.

Les clés de la performance

Il faut garder à l’esprit l’importance de la liaison entre le SGBDR et l’application. En voulant simplifier l’écriture de la couche DAO, les ORM ont rendu cette partie plus floue. En effet, un grand nombre de comportements nécessaires aux développeurs sont masqués derrière un paramétrage très complexe. Pour se rendre compte des requêtes exécutées, il est fortement conseillé d’activer les logs de l’ORM. Cette source d’informations est précieuse car elle permet de savoir exactement ce que fait l’ORM et à quel moment il le fait. Un bon paramétrage passe d’abord par une bonne compréhension de l’outil.

Toujours dans un souci de performance, Hibernate utilise un cache de premier niveau pour l’ensemble de ses requêtes. Mais il est également possible d’avoir recours au cache de second niveau utilisé cette fois-ci pour les classes persistantes. En fonction des données à mettre en cache, différentes stratégies peuvent être appliquées :

  • read-only : Dans le cas d’une lecture seule des données.
  • read-write : Pour des objets mis à jour par l’application (à bannir si l’application nécessite un niveau d’isolation transactionnelle sérialisable).
  • nonstrict-read-write : Si l’application a besoin de mettre à jour occasionnellement des objets.
  • transactional : Cette stratégie supporte un cache complètement transactionnel. Si sa valeur est true, les éléments n’expirent jamais. Dans le cas contraire les indications temporelles sont utilisées.

Une fois la stratégie de concurrence d’accès au cache déterminée, il est possible d’affiner le paramétrage pour chaque classe persistante. Les paramètres portent sur le nombre d’objets à stocker en mémoire ainsi que la durée de vie ou encore la règle de suppression des objets une fois la limite atteinte. Trois règles de suppression existent : LRU (Least Recently Used), FIFO (First In First Out) et LFU (Less Frequently Used).

Tous ces paramètres influent directement sur les performances applicatives et seul un bon paramétrage entraînera de bonnes performances.

Pour conclure

La mise en place d’une nouvelle spécification nécessite un processus très long. Durant cette période, chaque ORM subira de nombreuses évolutions et ces dernières ne pourront figurer dans la dernière version de la spécification. De plus, JPA n’étant que le plus petit dénominateur commun entre les principaux ORM du marché, certaines fonctionnalités présentes uniquement dans un ORM ne seront pas disponibles. C’est le cas de l’API « Criteria » ou encore « Example » d’Hibernate. D’autres fonctionnalités viennent s’ajouter à la liste et ce constat est identique pour les autres ORM.

Le choix d’utiliser la persistance avec ou sans JPA devient difficile. En effet, l’utilisation d’une couche d’accès aux données basée uniquement sur JPA permet de ne pas créer d’adhérence avec l’ORM assurant ainsi une plus grande portabilité à l’application. En revanche, ce choix se fait au détriment des fonctionnalités spécifiques des différents ORM.

Il faut donc voir les choses différemment. La spécification JPA peut être reconnue comme un gage de qualité par la communauté J2EE. Ainsi, elle apporte plus de crédibilité aux ORM et a un effet dopant pour ces derniers. De plus, la couche DAO étant dans la majorité des architectures déjà isolée du reste de l’application, un couplage fort entre cette couche et l’ORM n’est pas réellement problématique.