1. L' API JPA
1.1. Généralités
L'API JPA, fait partie de la spécification des EJB 3.0 dans Java Enterprise Edition 5 (Java EE 5); elle est développée par Sun pour offrir des fonctionnalités génériques à tous les ORMs (Object Relational Mapping), ce qui le rend indépendante de tout ORM particulier (comme Hibernate, TopLink, etc); cette API JPA est basée sur:
- un ensemble d'interfaces et de classes qui permettent de séparer l'utilisateur d'un service de persistance (votre application) et son fournisseur,
- un ensemble d'annotations pour préciser la mise en correspondance entre classes Java et tables d'une base de données relationnelles,
- un fournisseur de persistance (par exemple Hibernate),
- un fichier XML « persistence.xml » qui décrit les moyens de la persistance (fournisseur, datasource, etc.)
On utilise l'API JPA pour développer des applications java, au sein de serveurs d'application (applications Java EE) ou des applications Java SE (Java Standard Edition) permettant d' inter-agir avec des sources de données. L'un des éléments de base l'API JPA est le "JPA Entity" (identique aux beans Entity); au sein d'une application, une classe est considérée comme une JPA Entity si elle est précédée de l'annotation @Entity ou si elle est configurée dans un fichier XML (dans ce cas on parle de meta information de persistance).
Dans la plupart des architectures applicatives, une couche d’accès aux données appelée DAO (Data Access Object) est modélisée; dans une configuration classique, cette couche interroge directement le SGBDR à l’aide de pilote JDBC. Dans une configuration utilisant la spécification JPA couplée à un ORM qui l’implémente, 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 en l'occurrence les JPA Entity, forme «le contexte de persistance» appelé "unité de persistance. Au cours du cycle de vie de ce contexte de persistance, un objet JPA Entity peut prendre différents états:
- Etat «persisté»: on parle d'état persisté lorsque l’objet est géré par le contexte de persistance.
- Etat «non persisté»: en opposition à «persisté», cet état signifie qu’un objet n’est pas géré par le contexte de persistance.
- Etat «détaché»: on parle d'état détaché dans le cas d’un objet déjà persisté mais dont le contexte de persistance a été fermé.
Pour gérer le cycle de vie du contexte de persistance, la couche JPA dispose de l’interface «EntityManager» qui permet de gérer les Entité de persistance ( JPA ENTITY); cette interface possède un certain nombre de méthodes et les plus utilisées sont:
- void persist(Object entity); cette méthode insère l'objet dans le contexte de persistance. 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 lui passant comme paramètre une entité dont l'attribut représentant la clé est non null. La méthode lève alors l'exception EntityExistsException. - void remove(Object entity); supprime l'objet du contexte de persistance.
- <T> T merge(T entity); fusionne l'objet détaché avec celui de même identifiant présent dans le contexte de persistance courant. 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 parler 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.
- <T> T find(class<T> entityClass, Object primaryKey); ajoute dans le contexte de persistance l'objet correspondant à cette clé primaire dans la base; le type T permet à la couche JPA de savoir sur quelle table on fait la requête; en retour on a l'objet trouvé. 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, une instance EntityManager agit également comme un cache au dessus de la base de données qui 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.
L'interface EntityManager possède d’autres méthodes qui permettent 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; on peut aussi utiliser l'API Criteria pour faire des requêtes sur des objets JPA Entity. On peut également centraliser les requêtes 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é.
2. Bean Entity
2.1. Généralités
La notion de JPA Entity est l'élément central de l'API JPA et dans une application java utilisant JPA, chaque bean Entity( JPA Entity) représente une table dans une base de données relationnelle; chaque instance d'un bean Entity représente une ligne de la table associée au bean Entity dans la base de données. La persistance d'une instance d'un bean Entity est effective dans la base de données à travers les champs et les propriétés de celle-ci. Ces champs et ces propriétés servent pour mapper le bean Entity à une table dans la base de données.
2.2. Les exigences pour un bean Entity
Un bean Entity doit obéir aux exigences suivantes:
- Il doit être annoté par l'annotation "@javax.persistence.Entity".
- Il doit avoir un constructeur public ou privé sans argument; toutefois il peut avoir d'autres constructeurs.
- Il ne doit pas être déclaré final ou avoir de méthode ou une instance déclarée final.
- Si une instance d'un bean Entity est transmise par valeur en tant qu'un objet détaché à une autre méthode de l'application en cours, par exemple via un bean Session qui implémente une business interface, le bean Entity doit implémenter l'interface Serializable.
- Un bean Entity peut étendre toute sorte de classe java et vice-versa.
- Les variables d'instance persistantes doivent être déclarées private ou protected et peuvent être directement accessibles seulement par les méthodes du bean Entity. On accède à une instance du bean Entity par les accesseurs (Getter/setter) ou par les business méthodes.
2.3. Gestion d'un bean Entity
Un bean Entity au sein d'une application Java est prise en charge par une instance de l'interface "javax.persitence.EntityManger" et chaque instance de EntityManager est associée à un contexte de persistance; c'est dans le contexte de persistance que des instances des beans Entity sont créées, rendues persistantes ou supprimées. Dans l'interface EntityManager sont définies des méthodes qui permettent d'agir sur les instances des beans Entity.
2.3.1. L'interface EntityManager
Comme on l'a dit ci-dessus, une instance de l'interface EntityManager permet d'interagir avec l'unité de persistance d'une application Java utilisant l'API JPA; on peut au sein d'une unité de persistance créer, détruire, trouver dans une base de données relationnelles via la clé primaire des instances de beans Entity. Une instance de EntityManager est soit traitée au sein d'un serveur d'applications Java et dans ce cas la transaction est définie comme de type "JTA" soit au sein d'une simple application Java SE et la transaction est gérée par l'application elle même et son type est "RESOURCE_LOCAL" ( voir le type de la transaction au sein du fichier persistence.xml).
2.3.2. Bean Entity géré par un conteneur
Dans un serveur d'applications, le contexte de persistance est propagé automatiquement par le serveur d'applications à tous les composants de l'application impliquant une instance de l'interface EntityManager lors d'une transaction JTA (Java Transaction API - c'est une API dédiée à la gestion de transaction). Pour utiliser une instance de l'interface EntityManager, on l'injecte au sein de l'application; le composant de l'application au sein duquel on fait l'injection peut être un EJB ou un composant Java EE géré par CDI. on réalise cette injection par l'annotation "@PersistenceContext" comme suit:
1) Injection d'un EntityManager dans un EJB
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ejb.Stateless;
@Stateless
public class MaClasse {
@PersistenceContext(unitName="MaPersistenceUnit")
private EntityManager entityManager;
...
}
On voit bien qu'on est en présence d'un EJB Stateless ( voir l' annotation) et au sein de cet EJB on injecte l'EntityManager en le précédant de l'annotation "@PersistenceContext(...).
2) Injection d'un EntityManager dans un composant Java EE géré par CDI ( Injection de Contexte de Dependence)
import javax.enterprise.context.RequestScoped; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; /* Notez que ce composant Java EE porte l'annotation @RequestScope.
* Il est donc possible d'injecter en toute sécurité un EntityManager. */ @Named @RequestScoped public class MaClasse { @PersistenceContext(unitName="MaPersistenceUnit") private EntityManager entityManager; ... public void createEmployee(String fName, String lName) { Employee employee = new Employee(); employee.setFirstName(fName); employee.setLastName(lName); entityManager.persist(employee); } }
On voit bien que dans cette classe on a fait une injection de "entityManager" et que au besoin on peut l'utiliser pour persister une entity Empployee ayant deux champs.
Le serveur d'application initialise et ferme les différentes instances d'EntityManagerFactory et d'EntityManager. Autrement dit, le développeur d'application Java EE n'a pas à se préoccuper d'appeler les méthodes close() sur ces instances.
2.3.3. Bean Entity géré par une application
On peut avoir une application Java qui gère elle même chaque contexte de persistance et le cycle de vie des instances de l'interface EntityManager. Dans ce cas, chaque EntityManager crée un nouveau contexte de persistance isolé. L'EntityManager et son contexte de persistance associé sont créés et détruits explicitement par l'application; toutefois il faut tenir compte du faite que les instances d'EntityManager ne sont pas thread-safe contrairement aux instances "EntityManagerFactory"; donc pour créer des instances d'EntityManager on utilise la méthode "createEntityManager()"; on commence par injecter une instance de EntityManagerFactory à l'aide de l'annotation "PersistenceUnit" (voir code ci-dessous). Une fois qu'on a l'instance de EntityManagerFactory disponible, on l'utilise (une de ses méthodes en l'occurrence "createEntityManager()") pour créer une instance de l'interface EntityManager.
Le Contexte de persistance géré par l'application peut être utilisé avec JTA (Les transactions JTA se réfèrent aux transactions au sein d'un serveur d'applications JEE) ou avec la gestion des transactions par "Resource Local" (i.e une seule ressource transactionnelle, par exemple une connexion JDBC).
Tout d'abord, dans l'exemple ci-dessous on va illustrer la gestion des transactions par "Resource Local". Dans ce cas, le contexte de persistance est configuré comme RESOURCE_LOCAL; cela est précisé dans le fichier "persistence.xml" comme suit:
<persistence-unit name = "persistenceUnit" transaction-type =RESOURCE_LOCAL"> <non-jta-data-source> jdbc/dataSource </non-jta-data-source> <class> test.domain.TestEntity </class> </persistence-unit>
Une fois ces précisions faites dans le fichier "persistence.xml", on obtient une instance de EntityManager à partir d'une instance de EntityManagerFactory comme suit:
@PersistenceUnit EntityManagerFactory entityManagerFactory; ... EntityManager entityManager = entityManagerFactory.createEntityManager(); EntityTransaction entityTransaction = entityManager.getTransaction(); entityTransaction.begin(); entityManager.persist(entity); entityTransaction.commit(); entityManager.close();
On accède manuellement au gestionnaire de transactions JTA et on ajoute des informations de délimitation de transaction lors de l'exécution d'opérations avec des beans Entity. L'interface "javax.transaction.UserTransaction" définit des méthodes pour commencer, valider et renverser des transactions. On injecte une instance de UserTransaction en créant une variable d'instance annotée avec @Resource:
@Resource UserTransaction utx;
Pour démarrer une transaction, on fait appel à la méthode "UserTransaction.begin". Lorsque toutes les opérations concernant le bean Entity sont terminées, on appelle la méthode "UserTransaction.commit" pour valider la transaction. La méthode "UserTransaction.rollback" est utilisée pour annuler la transaction en cours.
L'exemple suivant montre comment gérer les transactions dans une application qui utilise un gestionnaire de beans Entity gérés par l'application:
@PersistenceUnit EntityManagerFactory emf; @Resource UserTransaction utx; ... EntityManager em = emf.createEntityManager(); try { utx.begin(); em.persist(SomeEntity); em.merge(AnotherEntity); em.remove(ThirdEntity); utx.commit(); } catch (Exception e) { utx.rollback(); }
(à suivre)...
3. Unité de persistance
Une unité de persistance est l'ensemble de tous les beans Entity qui sont gérés par des instances d'EntityManager dans une application. Cet ensemble de beans Entity représente les données contenues dans une seule base de données.
Les unités de persistance sont définies dans un fichier de configuration appelé persistence.xml; cidessous on a un exemple de définition:
<persistence> <persistence-unit name="OrderManagement"> <description>This unit manages orders and customers. It does not rely on any vendor-specific features and can therefore be deployed to any persistence provider. </description> <jta-data-source>jdbc/MyOrderDB</jta-data-source> <jar-file>MyOrderApp.jar</jar-file> <class>com.widgets.Order</class> <class>com.widgets.Customer</class> </persistence-unit>
</persistence>
Ce fichier définit une unité de persistance nommée OrderManagement, qui utilise une source de données "JTA-aware: jdbc/MyOrderDB". Les éléments jar-file et class spécifient les classes de persistance gérées: classes d'entités, classes incorporables et superclasses mappées. L'élément jar-file spécifie les fichiers JAR qui sont visibles pour l'unité de persistance empaquetée qui contiennent des classes de persistance gérées, tandis que l'élément de classe nomme explicitement les classes de persistance gérées.
Les éléments jta-data-source (pour les sources de données JTA) et non-jta-data-source (pour les sources de données non JTA) spécifient le nom JNDI global de la source de données à utiliser par le conteneur.
Le fichier JAR ou le répertoire dont le répertoire META-INF contient persistence.xml s'appelle la racine de l'unité de persistance. La portée de l'unité de persistance est déterminée par la racine de l'unité de persistance. Chaque unité de persistance doit être identifiée avec un nom qui est unique à la portée de l'unité de persistance.
Les unités persistantes peuvent être empaquetées dans un fichier WAR ou EJB JAR ou peuvent être emballées comme un fichier JAR qui peut ensuite être inclus dans un fichier WAR ou EAR:
- Si on colle l'unité persistante comme un ensemble de classes dans un fichier JAR EJB, le fichier "persistence.xml" doit être placé dans le répertoire "META-INF" du JAR du EJB.
- Si on classe l'unité de persistance comme un ensemble de classes dans un fichier WAR, le fichier "persistence.xml" doit se trouver dans le répertoire "WEB-INF/classes/META-INF" du fichier WAR.
- Si on colle l'unité de persistance dans un fichier JAR qui sera inclus dans un fichier WAR ou EAR, le fichier JAR doit être situé dans:
- Le répertoire "WEB-INF/lib" d'un war
- Le répertoire de la bibliothèque du fichier EAR.
(à suivre)...
4. Annotation d'une bean Entity
4.1. Principales annotations
Les principales annotations d'une classe JPA Entity sont @Entity, @Table et @Access. L'annotation @Entity nous indique que cette classe est une classe persistante. Elle peut prendre un attribut name, qui fixe le nom de ce bean Entity. Par défaut, le nom d'un bean Entity est le nom complet de sa classe. On peut fixer le nom d'un bean Entity par une constante, en utilisant un import statique.
L'annotation @Table permet de fixer le nom de la table dans laquelle les instances de cette classe vont être écrites. Cette annotation est particulièrement utile lorsque l'on doit associer un jeu de classes à des tables existantes. L'annotation @Table supporte plusieurs attributs:
- Les attributs catalog, schema et name : permettent de fixer les paramètres de la table utilisée.
- L'attribut @UniqueConstraints permet d'écrire des contraintes d'unicité sur des colonnes ou des groupes de colonnes.
Enfin, l'annotation @Access permet de fixer la façon dont l'EntityManager va lire et écrire les valeurs des champs de cette classe. Cette annotation ne prend qu'un seul attribut, qui ne peut prendre que deux valeurs : AccessType.FIELD et AccessType.PROPERTY. Dans le premier cas, les lectures/modifications se font directement sur les champs, dans le second elles passent par les getters/setters . Notons que cette annotation peut aussi être placée sur les champs ou getters d'une classe. On peut donc particulariser l'injection de valeur champ par champ.
(à suivre)...