JPA : une magie qui se mérite (retour aux sources de JPA)

Cet article fait partie d’une série sur les technologies incluses dans Java EE 6. Vous pouvez également lire l’article d’introduction de cette série : Java EE 6 ici et maintenant et celui sur JSF : JSF je t’aime, moi non plus

L’une des technologies les plus populaires dans le monde Java EE est sans aucun doute JPA. Il suffit de voir le nombre de livres et d’articles sur le sujet. Toutefois, si JPA est très répandu, il s’agit aussi de l’une des spécifications Java EE les moins bien maîtrisées par les développeurs et donc une source récurrente d’échecs des projets. Fort de ce constat je ne vais pas m’apesentir sur les nouveauté de JPA 2.0 dans ce post (en dehors de l’API criteria, celles-ci sont pluôt à la marge) pour me permettre de faire un retour aux sources sur JPA qui pourra profiter à certains. Ceux qui pensent maîtriser ces concepts pourraient être quand même intéressés par la fin de la’article qui traite de l’extended persistence context et de ses mérites.

Une “magie” complexe

La popularité de JPA et les échecs que cette techno engendre sont probablement liées au mélange de magie, de concepts compliqués qu’elle camoufle et de dissimulation de ces concepts au sein de frameworks destinés à faciliter son utilisation. La magie et les raccourcis offert par Spring (entre autres) permettent  de se lancer dans l’utilisation de l’outil sans avoir besoin de trop comprendre les couches sous-jacentes. Lorsque l’on arrive dans des cas concrets qui posent des problèmes, une grande partie des développeurs désinvestissent l’outil en critiquant ses limitations sans chercher à creuser plus avant. Parce que JPA nous fait sortir du confort du code pour aller nous battre avec des notions parfois mal digérés de gestion de transaction et base de données, il reste une énigme pour beaucoup. Certains développeurs pensent que le concept d’ORM en général et JPA en particulier permet de négliger SQL et les mécanismes bas niveau d’échange avec la BDD en les ramenant au rang de technologies inférieures. En pensant cela ils commettent une erreur : JPA rend l’utilisation de ces technologies plus facile et puissante mais ne nous permet pas des les occulter totalement. Il est un facilitateur pas une technologie de substitution.

Les 4 piliers de JPA

JPA s’inscrit autour de ces 4 notions fondamentales :

  • Les entités
  • Les Persistence Units (au runtime on les manipulera via la classe PersistenceManagerFactory)
  • L’Entity Manager (ou persistence manager)
  • Les transactions

Le schéma ci-contre illustre le fonctionnement de JPA et comment ces 4 éléments fonctionnent ensemble. Pour synthétiser :

  • La Persistence Unit organise les meta données qui définissent le mapping entre les entités et la base
  • La Persistence Manager Factory récupère ces metas données de la Persistence Unit et les interprètent pour créer des Persistence Manager
  • Le Persistence Mangager gère les échanges entre le code et la base de donnée, c’est à dire le cycle de vie des entités
  • Enfin, le travail du Persistence Manager est englobé dans une Transaction. Cette dernière peut éventuellement gérer les opérations de plusieurs Persistence Managers comme un traitement unitaire.

Sans faire un cours exhaustif, il parait intéressant de faire un point sur les concepts clés liés à chacune de ces notions clés

Les transactions

Les transactions ne faisant pas stricto sensu partie de JPA, mais étant une composante indispensable au fonctionnement et donc à la compréhension de cette technologie, il est intéressant de commencer notre tour d’horizon par un rappel sur les concepts liés au transactionnel. Les transactions sont en effet aussi importantes que les mécanismes de persistence décrits plus loin. Sans elles les données échangées avec la base risqueraient d’être corrompues ou incohérente. C’est pourquoi il est important de vérifier que le périmètre des transactions soit bien défini lors de l’interaction de votre code avec la base de données.

L’objet de ce post n’est pas de faire un cours magistral sur le transactionnel, mais il me semble intéressant de rappeler quelques concepts de base puisqu’on parle de base de données et de persistence.

Retour sur les différentes APIs transactionnelles

Une transaction est un regroupement d’instructions SQL effectuant un traitement fonctionnel atomique (par exemple l’ajout d’un article dans un panier d’achat accompagné d’une décrémentation des stock pour la référence concernée). Ce regroupement est orchestré à l’aide d’instructions SQL dédiées comme : BEGIN, COMMIT ou ROLLBACK. Bien sûr, si on ne souhaite pas agir à si bas niveau on aura plutôt recours en Java à des API chargées d’automatiser la gestion des transactions pour nous. A cette fin, nous disposons de trois possibilités :

  • Les transactions gérées par JDBC
  • Les transactions de type Resource-local
  • Les transactions gérées par  JTA
JDBC est hors sujet pour cet article, gardons juste à l’esprit que les appels JDBC sont toujours exécutés par défaut dans une transaction. Vous pouvez décider de gérer vous-même le début et la fin de celle-ci avec setAutocommit(false) dans une java.sql.Connection puis lorsque vous le souhaitez, lancer un commit() ou un rollback() à partir de cette même connexion. La gestion de la transaction reste simpliste et ne permettra pas de l’utiliser correctement au niveau de l’ORM,  mais elle est présente.
Les transactions de type Resource-Local sont elles gérées à un niveau supérieur, celui du Persistence Manager. Ce type de transaction rencontré dans Spring est axé sur la base et permet de gérer le cycle de vie de la transaction au niveau de l’ORM et d’automatiser facilement certains comportement (Rollback systématique en cas d’exception par exemple).
Enfin, les transactions JTA sont gérées par le container Java EE et permettent de travailler avec plusieurs datasources facilement. JTA est une spec Java EE, aussi on sera amené à l’utiliser dès qu’on voudra effectuer des tâches transactionnelles avec les EJB.
Pour conclure, gardez à l’esprit que la base de données ouvre et ferme une transaction à chaque opération si aucune démarcation de transaction n’est spécifiée dans votre code. C’est ce qui s’appelle la transaction implicite. Donc quand vous négligez de gérer les transactions, la base s’en charge à votre place à chaque instruction SQL y compris les lectures. Le mécanisme de la transaction étant assez coûteux, il est donc important d’opérer cette démarcation de manière explicite ne serait-ce que pour améliorer les performances de vos lectures.
Les entité et le Mapping

JPA permet donc d’associer des objets dans notre code à des données dans une ou plusieurs bases. Les metas données du mapping (sous forme d’annotations) apportent la souplesse indispensable qui évite d’avoir un modèle de données mémoire totalement calqué que le modèle de donnée relationnel dans la base. On pourra ainsi gérer les colonnes associées à un champ (avec @column), créer une entité sur plusieurs tables (avec @SecondaryTable) ou gérer des champs ne devant pas être stockés (avec @Transient). Bien sûr la magie n’est pas absolue et il peut arriver un moment où votre structure de base de données est trop complexe pour assurer un mapping satisfaisant et où le choix de JPA peut commencer à être remis en cause. Une notion qu’il est capitale de comprendre est la persistance transitive : les associations entre entités avec les fameux @OneToOne, @OneToMany ou @ManyToMany. Le débutant en JPA rencontrera très vite ses premiers problèmes avec ces concepts. Soit parce que les associations déclencheront des chargements en cascade et créeront des grappes d’objets monstrueuses en mémoire, soit parce qu’il aura eu recours au mécanisme de Lazy loading qui peut devenir un cauchemar si on ne maîtrise pas le cycle de vie des entités et les concepts liés au transactionnel. On se reportera à la documentation des annotations JPA d’Hibernate pour avoir une liste exhaustive des possibilités de mapping disponibles dans JPA 2.

La Persistence Unit

C’est le point de départ du moteur JPA. Elle permet de lister les entités à manager et la façon dont elles sont mappées vers la base. Elle indique également le type de transaction utilisée lors des échanges avec la base. Elle est constituée à partir de trois parties :

  1. Les meta données sur les entités (sous forme d’annotations, elles peuvent être surchargées par du XML)
  2. Le descripteur de la Persistence Unit : le fichier de configuration Peristence.xml qui permet de configurer la connexion à la base et la nature des transactions et des paramètres spécifiques à l’implémentation de JPA utilisée.
  3. La Persistence Manager Factory : Objet au runtime représentant la configuration complète de la persistence unit. Cette factory permet de créer un ou plusieurs persistence manager fournissant les services nécessaires à la gestions des entités

Mais n’oublions pas la plomberie JDBC sous JPA et la fameuse datasource, point de départ à la connexion vers la base…

JDBC et pool de connexion

JDBC reste donc la brique de base pour interagir avec la base de données et comme a priori votre application effectuera plusieurs requêtes simultanément vers la base vous avez besoin de fournir à JPA un pool de connexions JDBC. Dans un container Java EE tout cela se fait en général en définissant une data source, soit via un fichier (comme dans Glassfish ou JBoss) soit via une interface d’admin (comme dans Websphere). Cette data source est ensuite appelée via JNDI dans le fichier persistence.xml. La datasource consitue donc un pool de connexions JDBC. son rôle est de pouvoir vous fournir une connexion déjà “prête à l’emploi” (ouverte) quand vous voulez requêter la base et de faire le ménage quand un échange s’est mal passé (timeout, connexion mal fermée, etc…). Ce pool est fourni avec votre serveur d’application. exemple de data source JBoss :

DefaultDS jdbc:hsqldb:/Data/hypersonic/localDB org.hsqldb.jdbcDriver sa 5 20 0

La plupart des containers légers permettent de faire la même chose, quasiment tous fournissent un gestionnaire de Pool JDBC. Pour les utilisateurs de Spring, il y a dans le framework de quoi déclarer un bean Datasource auquel on pourra adjoindre le pool JDBC de son choix. Je vous laisse le soin de lire cette doc croustillante qui fourmille de trolls vintage sur Java EE, (heu… pardon J2EE). Toute pique mise à part, Spring sera très utile pour gérer cet aspect ainsi que les problématiques transactionnelles dans un environnement Java SE, l’alternative étant de créer ce pool dans votre code. Quoiqu’il en soit, on se gardera bien de définir sa connexion JDBC dans le fichier persistence.xml car celle-ci ne permet pas (en standard) de choisir la nature du pool utilisé. Ainsi si votre implémentation JPA est hibernate, déclarer votre connexion JDBC dans persistence.xml vous amènera à utiliser le pool de connexion Hibernate par défaut qui n’est pas recommandé en production. Des propriétés propres à Hibernate permettent dans persistence.xml d’activer d’autres gestionnaires de pool fournis par le framework comme C3PO ou Proxool. Ces gestionnaires sont également peu recommandés, on essaiera d’utiliser en priorité le gestionnaire de pool du serveur d’application.

Le fichier persistence.xml

Là non plus je ne rentrerai pas dans les détails de la configuration du fichier de persistence. Je donnerai juste deux versions de la même unité de persistence. L’une avec une gestion du transactionnel et de la datasource au niveau container :

org.hibernate.ejb.HibernatePersistence java:/DefaultDS

La persistence unit définie dans ce fichier exploite la Datasource définie plus haut. Elle utilise Hibernate comme implementation JPA d’où la propriété hibernate.dialect propre à cette implémentation. La deuxième PU, définie une connexion et un pool JDBC géré en local dans l’application (en l’occurence par Hibernate)

org.hibernate.ejb.HibernatePersistence

Spring propose une configuration en partie propriétaire dans son fichier de configuration pour la Datasource, puis dans persistence.xml, ce qui n’est pas toujours très clair. Comme on parle ici de Java EE , on se reportera à la documentation du Framework printanier qui fait pousser le XML,  pour plus d’informations sur ce dernier.

La gestion des persistence managers

La aussi il y a plusieurs possibilités pour gérer le cycle de vie des persistence manager JPA :

  1. Géré par l’application dans le code en créant des instances via le Persistence Manager Factory
  2. Géré par un framework du type Spring, Seam  (si on on est sur un serveur léger). Cette solution est techniquement équivalente à la première mais vous décharge d’avoir à gérer les instanciations
  3. Dans le cas d’un serveur Java EE, ceux-ci sont gérés par le container ce qui permet par exemple de partager les mêmes persistence units entre plusieurs war au sein d’un même EAR
En fonction de qui gère les persistences manager (le Serveur ou l’application) on pourra utiliser différents  modes transactionnels comme on l’a vu ci-dessus.
Techniquement cette gestion revient à savoir qui bootstrap JPA en transformant la persistent unit en persistence manager factory.

Le persistence manager Factory

Le chargement de la persistence unit (par le serveur ou l’application) donne lieu à la création d’un objet immuable : le persistence manager factory (ou entity manager factory) qu’on va récupérer automatiquement dans un context Java EE  via :

@PersistenceUnit private EntityManagerFactory pu;

si vous devez le faire à la main (sans un container ou framework qui le fait pour vous) cela donnera

EntityManagerFactory entityManagerFactory=Persistence.createEntityManagerFactory("my_pu");

Le code précédent bootstrap JPA et déclenche la lecture du Persistence Context “my_pu”. Ce code est comme on s’en doute extrêmement coûteux, aussi on fera en sorte de ne lancer qu’au démarrage de l’application. On travaillera rarement avec l’EntityManagerFactory et on préferera demander au container directement un EntityManager.

Au coeur du Persistence manager

Le persistence manager (ou entity manager, la littérature utilise l’un ou l’autre) est l’API responsable de gérer le cycle de vie des entités JPA. Au delà des échanges explicites avec la base de données,  il est en charge de vérifier l’état des entités qu’il manage pour propager les modifications automatiquement vers la base et gérer le cache  de niveau 1 permettant d’optimiser les accès SQL. Pour l’obtenir à partir du container on utilisera

@PersistenceContext private EntityManager em;

si on veut le faire à la main (après avoir récupéré l’entityManagerFactory avec le code ‘”à la main” précédent):

EntityManager em=entityManagerFactory.createEntityManager();

Dans ce cas gardez à l’esprit que vous devrez gérer le cycle de vie de l’entityManager vous-même.

Une fois cela fait, nous parvenons enfin au coeur de JPA avec le Persistence Manager dont les responsabilités sont les suivantes :

  • Gérer les instances des entités : l’API du Persistence manager permettent de gérer les entités. Cette API dispose des méthodes pour créer, chercher, requêter, mettre à jour et détruire les instances des entités. Ce faisant, il gère les quatre états possibles pour chaque objet entité : transient, persisté, détaché et détruit. (c.f. le schéma sur le cycle de vie ci-dessous)
  • Maintenir le context de persistance (Pesistence Context en VO) : Le contexte de persistance correspond à un cache mémoire des objets entité qui ont été chargés via le persistence manager. C’est le point d’entrée pour l’optimisation des performances dans la technologie JPA. vous le connaissez probablement aussi sous le nom de cache de premier niveau. Notez aussi que le terme Persistence Context est aussi parfois utilisé à la place de Persistence Manager (ou Entity Manager 😉 ) bien que ce ne soit pas exactement la même chose.
  • Réaliser des contrôles automatiques des objets managés (dirty checks dans le texte) : l’état des objets présents dans le contexte de persistance est automatiquement surveillé pendant toute la durée de vie de ce dernier. Quand un événement de “flush” intervient sur le persistence manager, les modifications detectées par ce mécanisme sont poussées vers la base de données  sous forme de de commandes SQL Lorsque une instance d’entité entre dans l’état “détaché” (detached) ses modifications ne sont plus surveillées.

Le persistence manager travaille à plus haut niverau que le SQL. Grace aux annotations de mapping, il comprend que les données transitant depuis la base ont une structure objet (l’entité) et que cette structure à un cycle de vie (voir ci-contre).

Les instances d’entité débutent leur vie comme étant non managées (stade transient), puis elles deviennent managées ce qui leur permet d’être synchronisées avec la base de données. Si elles sont détruites cette synchronisation devient une suppression. Lorsqu’elles sont supprimées de la base ou que le persistence context est fermé elles passent à l’état détaché et ne sont plus gérées par le persistence manager.
L’atout majeur du persistence manager est probablement le persistence context qui donne tout son intérêt à JPA. En effet, il peut optimiser ses échanges avec la base de données en les évitant lorsqu’il detecte qu’une instance demandée est déjà dans le persistence context (souvenez-vous, c’est le cache de niveau 1). Plus intéressant encore: le persistence manager vous garantit l’unicité de chaque instance d’entité présente dans le persistence context grâce à son identifiant et son type. Conséquence : l’entity manager peut surveiller l’état des instances des entités et peut propager leurs modifications vers la base éventuellement même en cascade pour les entités ou collections associées. En outre, tant que le persistence manager reste ouvert, il est possible d’effectuer des chargements tardifs (le fameux Lazy Loading) de collections associées sans avoir à requêter le base.
Ces fonctionnalités font tout l’intérêt du persistence manager et le rendent bien plus riche que la simple couche d’accès aux données à laquelle on l’assimile trop souvent.
Cela dit pour pouvoir tirer partie de ces fonctionnalités il est important de savoir (et pouvoir) gérer le scope du persistence context.

Scoper le persistence context : une approche trop souvent négligée

Le persistence Manager (et par association le contexte de persistence) est souvent victime d’une méconnaissance des développeurs qui le pensent lié à la connexion avec la base ou à la durée de vie d’une transaction.  Cette erreur provient principalement des architectures stateless dont le framework Spring a fait la promotion depuis ses débuts. A cause de cette vision simpliste et restrictive, beaucoup de développeurs considèrent comme une mauvaise pratique (voire ignorent complètement) le fait de laisser ouvert le persistence manager pour une durée supérieure à une transaction ou une requête HTTP et correspondant à un cas d’utilisation de leur application. En fait le persistence manager est bien plus polyvalent que l’approche hyper partielle et pauvre prêchée par Spring. Oui, il peut être stateful (le mot est lâché), et donc laissé ouvert le temps de réaliser un cas d’utilisation complet (plusieurs écrans et actions de l’utilisateur). Contrairement à ce qu’on pourrait craindre, le persistence manager ne laissera pas la connexion vers la base ouverte tout le temps de sa durée de vie mais saura se reconnecter de manière transparente si le besoin s’en fait sentir.

Le mauvais réglage du scoping du Persistence Manager est la cause principale des critiques adressées à JPA. Avoir recours à des outils ou patterns qui ne permettent que de l’associer à la requête HTTP ou à la transaction en cours amène les développeurs à bricoler des solutions de contournement qui sont autant de mauvaises pratiques : comme le hideux pattern Open Session in view, une redéfinition du mapping JPA pour mettre en Fetch.Eager des associations qui auraient du rester lazy ou du requetage HSQL en pagaille pour  récupérer la grappe d’objets nécessaire pour traiter la requête et ses suites (comme des échanges Ajax par exemple). Comme je l’ai écrit dans de précédents articles, si Spring a été un apport précieux pour la mise en place de bonnes pratiques en terme d’architecture, on atteint là ses limites qui sont liées à des choix idéologiques de ses concepteurs qui n’ont autorisé pendant très longtemps qu’une approche purement stateless des architectures.

Ce qu’on doit faire pour que le persistence manager ait une durée de vie supérieure à la requête HTTP, c’est le gérer en mode stateful. C’est cette approche qui permettra de gérer un scope conversation comme le fait Seam 2 depuis 2006 et comme le propose CDI en standard dans Java EE 6. Cette approche nécessite de faire attention où le persistence manager est stocké, car il ne doit pas être dans un scope partagé (scope application par exemple) puisqu’il n’est pas thread safe. Voyons comment Java EE 6 gère ce mécanisme.

Le peristence context étendu

Depuis Java EE 5,  nous avons la possibilité de créer un persistence context étendu. Comme dit, plus haut, c’est un persistence context qui reste actif au dela de la transaction (mode par défaut du persistence context).

Avoir recours à un Persitence context étendu :

  • Permet de gérer proprement le lazy loading des entités ou collections associées
  • Evite d’avoir à merger des entités détachées pour les synchroniser avec la base
  • Vous assure de n’avoir qu’une seule référence à un objet pour un id d’entité donné
  • Fonctionne avec les mécanisme d’Optimistic Locking pour supporter les unités de traitement à durée de vie longue (l’annotation JPA @Version est très utile pour ce dernier point)

Le persistence context étendu doit donc résider dans un scope stateful, et non partagé. La session ou la conversation pourraient faire l’affaire, mais la gestion de son cylce de vie resterait un peu fastidieuse. C’est pourquoi, la meilleure place pour le gérer est de le placer au sein d’un EJB Session Sateful (Stateful Session Bean ou SFSB).

On ne va pas trop entrer dans les détails des EJB ici (je garde cela pour mon prochain post), il faut juste savoir qu’un SFSB est géré par un container spécifique qui n’a rien à voir au conteneur servlet. Notre Bean a donc un cycle de vie indépendant des scopes servlet mais pourra être manipulé depuis ces fameux scopes soit en étant injecté via @EJB, soit, encore mieux en étant injecté ou directement utilisé via CDI. Le persistence context étendu ainsi utilisé ne sera fermé que lorsque le SFSB sera recyclé.  Nous reviendrons sur tout ça dans un post ultérieur. Pour le moment voici un petit exemple que j’ai emprunté à Adam Bien .

@Stateful @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) @Named @ConversationScoped public class OrderGateway{ @PersistenceContext(type=PersistenceContextType.EXTENDED) EntityManager em; private Load current; public Load find(long id){ current = em.find(Load.class, id); return current; } public Load getCurrent() { return current; } public void create(Load load){ em.persist(load); current = load; } public void remove(long id){ Load ref = em.getReference(Load.class, id); em.remove(ref); } @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void save(){ //nothing to do } }

La ligne 1 fait de notre classe un SFSB (EJB Stateful), la deuxième ligne désactive le support transactionnel sur l’EJB en question, la ligne 3 fait que la version CDI de cet EJB pourra être utilisé dans une page JSF via l’expression language. Et la quatrième stock le composant CDI dans le scope conversation.

On voit que dans le SFSB on inject un entity manger étendu en ligne 7. Diverses méthodes permettent de chercher, créer et supprimer des entités et en ligne 31 on annote une methode save() avec l’attribut REQUIRES_NEW qui va créer une transaction et la fermer dans la foulée commitant ainsi toutes les modifications effectuées (rappelez-vous l’entity manager empile les  modification jusqu’à ce qu’une transaction lui permette de pousser ces modifications). Ce scénario simplifié permet d’imaginer les possibilités offertes par l’extended persistence context.

Conclusion

L’empilement de technologies masquées par JPA  constitue à la fois l’origine de son succès et la principale raison des critiques que la spécification essuie. Les cas d’utilisations restreints mis en avant par Spring en ne permettant que de concevoir des architecture stateless pour les services, sont également à l’origine de pas mal de galères sur JPA. Pour utiliser cette technologie au mieux je ne vous donnerai que 3 conseils :

  • Apprenez Java EE 6, ça ouvre des horizons.
  • Si vous ne pouvez pas travailler avec Java EE, utilisez Seam 2 qui marche très bien sur des conteneurs léger type Tomcat et qui reprend ces concepts.
  • Lisez la doc de JPA (vous pouvez commencer par ici) et non pas la version Springifiée de celle-ci.

Bibliographie

Cet Article s’appuie sur les livres suvants