Nous avons vu que l'API JDBC nous permet d'écrire des programmes Java qui interagissent avec des bases de données.
JDBC nous assure que le code Java sera semblable quel que soit le SGBDR utilisé (mais le code SQL pourra bien sûr être
différent en exploitant telle ou telle fonctionnalité non standard fournie par le SGBDR).
Néanmoins, JDBC a quelques inconvénients :
l'API est verbeuse et répétitive. Pour un programme de quelques centaines de lignes de code, elle se révèle
très efficace. Mais pour des applications plus volumineuses, la quantité de code nécessaire (notamment SQL) peut devenir une source
de ralentissement du développement.
la gestion des nombreuses ressources (Connection, Statement, ResultSet) est une source permanente de bugs pour les développeurs. Il est donc très facile d'écrire
des applications qui perdent des ressources.
JDBC n'offre qu'un service limité : un système d'échange avec une base de données (même s'il le fait très bien).
Les ORM (Object-Relational Mapping)
Les ORM sont des frameworks qui, comme l'indique leur nom, permettent de créer une correspondance entre un modèle objet et
un modèle relationnel de base de données. Un ORM fournit généralement les fonctionnalités suivantes :
génération à la volée des requêtes SQL les plus simples (CRUD)
prise en charge des dépendances entre objets pour la mise en jour en cascade de la base de données
support pour la construction de requêtes complexes par programmation
Java EE fournit une API standard pour l'utilisation d'un ORM : JPA (Java Persistence API) (JSR-317).
Il existe plusieurs implémentations open source qui respectent l'API JPA : EclipseLink (qui est aussi l'implémentation de référence),
Hibernate (JBoss - Red Hat), OpenJPA (Apache).
Toutes ces implémentations sont bâties sur JDBC. Nous retrouverons donc les notions de pilote, de data source et d'URL de connexion
lorsqu'il s'agira de configurer l'accès à la base de données.
Les entités JPA
JPA permet de définir des entités (entities). Une entité est simplement une instance d'une classe qui sera persistante (que
l'on pourra sauvegarder dans / charger depuis une base de données relationnelle). Une entité est signalée par l'annotation
@Entity sur la classe.
De plus, une entité JPA doit disposer d'un
ou plusieurs attributs définissant un identifiant grâce à l'annotation
@Id
. Cet identifiant correspondra à la clé primaire dans la table associée.
Il existe un grand nombre d'annotations JPA servant à préciser comment la correspondance doit être
faite entre le modèle objet et le modèle relationnel de base de données. Il est possible
de déclarer cette correspondance à l'aide du fichier XML orm.xml. Cependant, la plupart de développeurs
préfèrent utiliser des annotations. Les tableaux ci-dessous résument les annotations les plus simples et les plus
utiles pour commencer à utiliser JPA :
Définit qu'une classe est une entité. Le nom de l'entité est donné par l'attribut name (en son absence le nom de l'entité correspond au nom de la classe).
Définit l'attribut qui sert de clé primaire dans la table. Il est recommandé au départ d'utiliser
un type primitif, un wrapper de type primitif ou une String pour représenter un id.
Pour les clés composites, la mise en œuvre est plus compliquée. Afin de ne pas se compliquer inutilement
la tâche, il vaut mieux prévoir une clé technique simple pour chaque entité.
Définit un mapping simple pour un attribut (par exemple VARCHAR pour String). Si on ne souhaite pas changer
la valeur des attributs par défaut de cette annotation, alors il est possible de ne pas la spécifier
puisqu'elle représente le mapping par défaut.
Pour un attribut de type java.util.Date et java.util.Calendar, cette annotation
permet de préciser le type de mapping vers le type SQL (DATE, TIME ou TIMESTAMP).
Indique qu'un attribut ne doit pas être persisté. Cet attribut ne sera donc jamais pris
en compte lors de l'exécution des requêtes vers la base de données.
Permet de définir les informations sur la table représentant cette entité en base de données. Il
est possible de définir le nom de la table grâce à l'attribut name. Par défaut le nom de la table
correspond au nom de l'entité (qui par défaut correspond au nom de la classe).
Indique la stratégie à appliquer pour la génération de la clé lors de l'insertion d'une entité en base. Les valeurs possibles sont données
par l'énumération GenerationType.
Si vous utilisez MySQL et la propriété autoincrement sur une colonne, alors vous devez utiliser GenerationType.IDENTITY (ce sera
le cas pour les exemples de ce cours).
Si vous utilisez Oracle et un système de séquence, alors vous devez utiliser GenerationType.SEQUENCE et préciser le nom
de la séquence dans l'attribut generator de @GeneratedValue.
Permet de déclarer des informations relatives à la colonne sur laquelle un attribut doit être mappé.
Si cette annotation est absente, le nom de la colonne correspond au nom de l'attribut.
Avec cette annotation, il est possible de donner le nom de la colonne (l'attribut name) mais également
si l'attribut doit être pris en compte pour des requêtes d'insertion (l'attribut insertable) ou
de mise à jour (l'attribut updatable). Certains outils sont capables d'exploiter les annotations pour créer
les bases de données. Dans ce cas, d'autres attributs sont disponibles pour ajouter toutes les contraintes nécessaires
(telles que length ou nullable) et donner ainsi une description complète de la colonne.
À l'entité JPA ci-dessus, on pourra faire correspondre la table MySQL :
L'EntityManager
Les annotations JPA que nous avons vues dans la section précédente, ne servent à rien si elle ne sont pas
exploitées programmatiquement. Dans JPA, l'interface centrale qui va exploiter ces annotations est
l'EntityManager.
À partir d'une instance d'EntityManager, nous allons pouvoir manipuler les entités afin de les créer,
les modifier, les charger ou les supprimer. Pour cela, nous disposons de six méthodes :
find
persist
merge
detach
refresh
remove
L'EntityManager va prendre en charge la relation avec la base de données et la génération des requêtes SQL nécessaires.
De plus, l'implémentation JPA se charge d'extraire ou au contraire de positionner les attributs dans l'instance de l'entité. Par exemple,
un appel à find retourne bien une instance de la classe spécifiée par le premier paramètre. Cette instance aura ses attributs
renseignés à partir des valeurs des colonnes sur lesquelles ils ont été mappés.
Pour les opérations qui modifient une entité (telles que persist ou remove), il faut que l'appel se fasse
dans le cadre d'une transaction. Grâce à la méthode EntityManager.getTransaction(), il est possible de récupérer la
transaction est de gérer la démarcation comme ci-dessous :
Attention cependant à ne pas croire que JPA est simplement un framework pour générer du SQL. Une des difficultés dans la maîtrise de JPA
consiste justement à comprendre comment il gère le cycle de vie des entités indépendamment de la base de données. Ainsi, on ne retrouve
pas sur l'interface EntityManager des noms de méthodes qui correspondent aux instructions SQL INSERT,
SELECT, UPDATE et DELETE. Il ne s'agit pas d'un effet de style, les méthodes
pour manipuler les entités ont un comportement qui dépasse la simple exécution de requêtes SQL.
À votre avis
Quelles sont les requêtes SQL exécutées par le code ci-dessous ?
Un EntityManager cherche à limiter les interactions inutiles avec la base de données. Ainsi, tant qu'une transaction
est en cours, le moteur JPA n'effectuera aucune requête SQL, à moins d'y être obligé pour garantir l'intégrité des données.
Il attendra si possible le commit de la transaction. Ainsi si une entité est créée puis modifiée au cours de la même transaction,
plutôt que d'exécuter deux requêtes SQL (INSERT puis UPDATE), l'EntityManager attendra la fin de la transaction pour
réaliser une seule requête SQL (INSERT) avec les données définitives.
La méthode persist
La méthode persist ne se contente pas d'enregistrer une entité en base, elle positionne également la valeur de l'attribut
représentant la clé de l'entité. La détermination de la valeur de la clé dépend de la stratégie spécifiée par
@GeneratedValue.
L'insertion en base ne se fait pas nécessairement au moment de l'appel à la méthode persist (on peut toutefois
forcer l'insertion avec la méthode EntityManager.flush()). Cependant, l'EntityManager garantit que des appels
successifs à sa méthode find permettront de récupérer l'instance de l'entité.
C'est une erreur d'appeler la méthode EntityManager.persist en passant une entité dont l'attribut représentant
la clé est non null. La méthode jette alors l'exception EntityExistsException.
La méthode find
La méthode EntityManager.find (Class<T>, Object) permet de rechercher une entité en donnant sa clé primaire.
Un appel à cette méthode ne déclenche pas forcément une requête SELECT vers la base de données.
En effet, un EntityManager agit également comme un cache au dessus de la base de données. Ainsi, il garantit l'unicité
des instances des objets. Si la méthode find est appelée plusieurs fois sur la même instance d'un EntityManager
avec une clé identique, alors l'instance retournée est toujours la même.
La méthode merge
La méthode EntityManager.merge(T) est parfois considérée comme la méthode permettant de réaliser les UPDATE
des entités en base de données.
Il n'en est rien et la sémantique de la méthode merge est très différente. En fait, il n'existe pas
à proprement parlé de méthode pour réaliser la mise à jour d'une entité. Un EntityManager surveille les entités
dont il a la charge et réalise les mises à jour si nécessaire au commit de la transaction. Par exemple le code ci-dessous
suffit à déclencher une requête SQL UPDATE :
Si un EntityManager détecte automatiquement les modifications des entités dont il a la charge, à quoi peut donc servir la méthode EntityManager.merge(T) ?
En fait si vous créez vous même une instance d'une entité et que vous positionnez la clé, cette entité n'est gérée par aucun EntityManager.
Pour qu'un EntityManager prenne en compte votre entité, il faut appeler la méthode merge :
L'inverse de la méthode EntityManager.merge(T) est EntityManager.detach(Object) qui annule la gestion
d'une entité par l'EntityManager.
La méthode detach
Comme son nom l'indique, la méthode EntityManager.detach(Object) détache une entité, c'est-à-dire que
l'instance passée en paramètre ne sera plus gérée par l'EntityManager. Ainsi, lors du commit de la transaction,
les modifications faites sur l'entité détachée ne seront pas prises en compte.
La méthode refresh
La méthode EntityManager.refresh(Object) annule toutes les modifications faites sur l'entité durant
la transaction courante et recharge son état à partir des valeurs en base de données.
La méthode remove
La méthode EntityManager.remove(Object) supprime une entité. Si l'entité a déjà été persistée en base de données,
cette méthode entraînera une requête SQL DELETE.
Obtenir un EntityManager
Comme pour JDBC, le recours à un conteneur Java EE n'est pas nécessaire pour utiliser JPA. Ainsi l'initialisation et la
méthode pour récupérer un EntityManager va être différente selon que le code s'exécute dans une application
"simple" ou dans une application déployée dans un serveur Java EE.
Quel que soit le contexte, il faut fournir à l'implémentation de JPA un fichier XML de déploiement nommé persistence.xml.
Ce fichier doit se situer dans le répertoire META-INF et être disponible dans le classpath à l'exécution. Dans un projet Maven, il
suffit de créer ce fichier dans le répertoire src/main/resources/META-INF du projet (créez les répertoires manquants si nécessaire).
Dans ce fichier, on déclare une ou plusieurs unités de persistance grâce à la balise <persitence-unit>.
Chaque unité de persistance est identifiée par un nom et contient la liste des classes entités gérées par cette unité. La
balise <properties> permet de spécifier des propriétés propres à une implémentation de JPA et que
indique comment se connecter au SGBDR.
Dans une application Java
Pour une application Java "simple", il faut utiliser la classe Persistence.
Grâce à cette classe, nous allons pouvoir créer une instance de EntityManagerFactory.
Cette dernière, comme son nom l'indique, permet de fabriquer une instance d'EntityManager.
Pour des raisons de performance, une seule instance d'EntityManagerFactory
devrait être créée par unité de persistance et par application.
Par contre, une instance d'EntityManager n'est pas prévue pour être conservée trop longtemps
(dans une application Web, sa durée de vie se limite généralement au traitement de la requête).
De plus, un EntityManager n'est pas conçu pour être utilisé dans un environnement concurrent.
Pour des applications multi-threadées, on utilisera une instance d'EntityManager par thread.
Les instances d'EntityManagerFactory et d'EntityManager représentent des ressources
système et doivent être fermées par un appel à leur méthode close() dès qu'elles ne sont plus utiles.
Dans un serveur Java EE
Pour une application déployée dans un serveur d'application Java EE, l'initialisation de JPA est un peu
différente car le serveur utilise la notion de DataSource JDBC et gère les transactions avec JTA
(Java Transaction API). Le fichier
persistence.xml est toujours présent mais il ne déclare plus une URL JDBC mais un DataSource.
La changement important consiste dans l'utilisation de la balise <jta-data-source>
qui donne le nom de la DataSource. Pour un déploiement dans TomEE, il faudra, par exemple,
ajouter la déclaration de cette DataSource dans le fichier resources.xml comme nous l'avons
vu dans le chapitre consacré à JDBC.
Il est possible d'obtenir une instance d'EntityManagerFactory par injection dans n'importe quel
composant Java EE (servlet, managed bean CDI, EJB) grâce à l'annotation
@PersistenceUnit :
Il est également possible d'obtenir un EntityManager grâce à l'annotation
@PersistenceContext.
Attention, un EntityManager n'est pas thread-safe. Il ne doit pas être injecté dans un
composant Java EE qui est utilisé dans des accès concurrents. Les beans CDI de portée requête (@RequestScope)
et la plupart des EJB sont des composants Java EE dans lesquels on peut injecter en toute sécurité un EntityManager.
Dans un environnement Java EE, il est de la responsabilité du serveur d'application d'initialiser et de fermer les
différentes instances d'EntityManagerFactory et d'EntityManager. Autrement dit, le développeur
de composant n'a pas à se préoccuper d'appeler les méthodes close() sur ces instances.
JPA et JTA
JTA (Java Transaction API) est une API dédiée à la gestion de la transaction. Cette API ne se limite pas aux transactions
avec des SGBDR mais a pour but d'offrir une interface unique pour tous les systèmes transactionnels. Ainsi, un SGBDR
n'est plus qu'un système transactionnel parmi d'autres.
L'inconvénient de l'utilisation de JTA est qu'il rend obsolète la méthode EntityManager.getTransaction(). Ainsi,
ce qui a été dit précédemment sur la démarcation de la transaction avec JPA n'est pas applicable pour les sources
de données utilisant le support de JTA.
Dans TomEE, une DataSource gérée par le serveur doit utiliser obligatoirement JTA pour fonctionner.
Il faut donc penser à activer le support JTA dans la déclaration de la DataSource
Pour signaler la démarcation transactionnelle, il existe deux solutions :
Soit on utilise un forme programmatique grâce à l'objet UserTransaction
Pour la méthode programmatique, une instance de cet object peut être récupérée par injection grâce à l'annotation @Resource dans un composant Java EE.
L'utilisation de l'API JTA est très verbeuse. De plus, chaque méthode peut jeter plusieurs exceptions et il n'est pas toujours facile de savoir comment les traiter.
En général, il est plus simple d'opter pour une approche déclarative (basée par exemple sur les EJB).
Amélioration de code
L'amélioration (enhancement) de code est une technique pour ajouter des instructions dans le code produit
afin d'enrichir le comportement d'une classe. Cette technique est largement utilisée par les implémentations
de JPA afin, notamment, de détecter les modifications d'état d'une entité ou d'exécuter des chargements différés (Cf. partie 2).
Dans les implémentations de JPA, l'amélioration de code est généralement réalisée grâce à un des trois procédés suivants :
Après la compilation, on utilise un utilitaire pour modifier les fichiers class des entités. Comme
la compilation Java traduit le code source en byte code, l'amélioration se fait par modification directe
du byte code dans chaque fichier.
Au moment du chargement du byte code par la JVM, il est possible de l'intercepter et donc de le modifier
à la volée. Cette opération est possible car Java est un langage avec une édition dynamique des liens.
Au moment du chargement du byte code par la JVM, il est possible de créer à la volée une classe héritant de la classe que l'on souhaite
améliorer en redéfinissant (override) les méthodes souhaitées.
L'amélioration de code est cependant une technique lourde à mettre en place. Une amélioration après compilation
nécessite les outils adéquats (pas toujours bien intégrés dans les IDE et/ou les outils de build).
Une amélioration au chargement des classes par la JVM impose des conditions de mise en place par la JVM elle-même pour
des raisons de sécurité. Enfin, la méthode par génération d'une classe fille est la plus simple à mettre en place mais
celle qui est le plus source de bugs : le développeur croit manipuler une instance de sa propre classe alors
qu'au moment de l'exécution, il s'agira d'une instance d'une classe générée à la volée.
OpenJPA, par exemple, supporte les trois techniques avec plus ou moins de facilité pour les développeurs d'application.
La page du site d'OpenJPA traitant de ce sujet est accessible ici.
OpenJPA : amélioration après la compilation
Il n'existe plus de plugin Eclipse maintenu pour une prise en charge de l'amélioration de code dans cet IDE.
Il existe cependant un plugin Maven mais qui nécessite de réaliser la compilation par Maven (et donc en dehors de l'IDE).
Pour un projet géré par Maven, il suffit d'ajouter dans le pom.xml le plugin suivant :
OpenJPA : amélioration à l'exécution
L'amélioration du code au moment du chargement de la classe par la JVM évite l'étape supplémentaire
au moment de la compilation sans réelle pénalité à l'éxécution. Cependant, cette option est rendue
compliquée pour des raisons de sécurité. En effet, il n'est pas souhaitable que du code malveillant
puisse modifier à son gré les classes chargées par la JVM. En Java, on spécifie un agent qui est
passé en argument au moment du lancement de l'application Java.
TomEE fournit son propre agent au démarrage du serveur qui permet l'amélioration de code des entités JPA. Par contre, pour une application
standalone, il est nécessaire de passer l'option -javaagent:[lien vers le jar openjpa] au lancement de la JVM.
OpenJPA : amélioration par héritage
L'amélioration de code par héritage est une technique supportée par OpenJPA mais officiellement déconseillée. En effet,
elle peut entraîner divers bugs et limitations. Il est cependant possible d'activer cette technique dans le fichier
persistence.xml en ajoutant la propriété suivante :