Java EE - EPSI POE mars 2017 - David Gayerie Licence Creative Commons

Java Persistence API (JPA) 1/2

  1. Les ORM (Object-Relational Mapping)
  2. Les entités JPA
  3. L'EntityManager
  4. Obtenir un EntityManager
  5. JPA et JTA
  6. Amélioration de code

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 :

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 :

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.

Un exemple de classe entité avec la déclaration de son identifiant

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Individu {

  @Id
  // Permet de définir la statégie de génération
  // de la clé lors d'une insertion en base de données.
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }
}

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 :

Les annotations orientées entité
Nom Description
@Entity 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).
@Id 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é.
@Basic 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.
@Temporal 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).
@Transient 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.
@Lob Indique que la colonne correspondante en base de données est un LOB (large object).
Les annotations orientées base de données
Nom Description
@Table 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).
@GeneratedValue 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.
@Column 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.
Un exemple plus complet de classe entité

import java.util.Calendar;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;

@Entity
@Table(name="individu")
public class Individu {

  @Id
  @Column(name="individuId")
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;

  @Basic
  @Column(length = 30, nullable=false)
  private String nom;

  @Basic
  @Column(length = 30, nullable=false)
  private String prenom;

  @Column(length = 3, nullable=false)
  private Integer age;

  @Temporal(TemporalType.TIMESTAMP)
  @Column(updatable = false)
  private Calendar dateAdhesion;

  @Temporal(TemporalType.TIMESTAMP)
  @Column(insertable = false)
  private Calendar dateModification;

  @Lob
  @Basic(fetch=FetchType.LAZY)
  private byte[] image;

  @Transient
  private Abonnement abonnement;

  // les getter/setter ont été omis pour faciliter la lecture
}

À l'entité JPA ci-dessus, on pourra faire correspondre la table MySQL :


CREATE TABLE `individu` (
  `individuId` int NOT NULL AUTO_INCREMENT,
  `nom` varchar(30) NOT NULL,
  `prenom` varchar(30) NOT NULL,
  `age` int(3) NOT NULL,
  `dateAdhesion` TIMESTAMP,
  `dateModification` TIMESTAMP,
  `image` BLOB,
  PRIMARY KEY (`individuId`)
);

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 :

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.

Exemples d'appel à l'EntityManager

  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance
  Individu individu = new Individu();
  individu.setPrenom("John");
  individu.setNom("Smith");
  individu.setAge(25);
  
  // Demande d'insertion dans la base de données
  entityManager.persist(individu);
  
  // Demande de chargement d'une entité.
  // Le second paramètre correspond à la valeur de la clé de l'entité recherchée.
  individu = entityManager.find(Individu.class, 2);
  
  // Demande de suppression (delete)
  entityManager.remove(individu);

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 :

Gestion de la transaction avec un EntityManager


  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance
  
  entityManager.getTransaction().begin();
  boolean transactionOk = false;
  try {
    // ..
    
    transactionOk = true;
  }
  finally {
    if(transactionOk) {
      entityManager.getTransaction().commit();
    }
    else {
      entityManager.getTransaction().rollback();
    }
  }

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 ?


  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance
  
  Individu individu = new Individu();
  individu.setNom("David");
  individu.setPrenom("Gayerie");
  individu.setAge(39);
  
  entityManager.getTransaction().begin();
  boolean transactionOk = false;
  try {
    entityManager.persist(individu);
  
    individu.setAge(40);

    entityManager.merge(individu);
  
    entityManager.remove(individu);
    
    transactionOk = true;
  }
  finally {
    if(transactionOk) {
      entityManager.getTransaction().commit();
    }
    else {
      entityManager.getTransaction().rollback();
    }
  }

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.


  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance

  Individu individu  = entityManager.find(Individu.class, 1);
  // Pour le second appel à find, aucune requête SQL n'est exécutée. 
  // L'EntityManager se contente de retourner la même instance que précédemment.
  Individu individu2 = entityManager.find(Individu.class, 1);
  
  // individu == individu2
				

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 :

Mise à jour implicite d'une entité

  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance
  
  entityManager.getTransaction().begin();
  try {
    Individu individu = entityManager.find(Individu.class, 1);
    if (individu != null) {
      individu.setAge(individu.getAge() + 1);
    }
    // Si l'age de l'individu a été incrémenté, JPA est
    // capable de le détecter et de déclencher un UPDATE 
    // au moment du commit.
    entityManager.getTransaction().commit();
  }
  catch (RuntimeException e) {
    entityManager.getTransaction().rollback();
    throw e;
  }

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 :

Utilisation de la méthode merge

  EntityManager entityManager = ... // nous verrons plus loin comment obtenir une instance
  
  entityManager.getTransaction().begin();
  
  Individu individu = new Individu();
  // on positionne explicitement l'id de l'entité
  individu.setId(1);
  
  try {
    // il est très important de remplacer notre instance
    // par celle retournée par l'EntityManager après un merge.
    individu = entityManager.merge(individu);
    // l'instance de individu contient bien l'âge stocké en base
    // de données (l'appel à merge à récupérer l'information)
    individu.setAge(individu.getAge() + 1);

    // JPA est capable de détecter que l'age de l'individu a été modifié
    // et qu'il faut réaliser un UPDATE SQL au moment du commit.
    entityManager.getTransaction().commit();
  }
  catch (RuntimeException e) {
    entityManager.getTransaction().rollback();
    throw e;
  }

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).

Contenu du fichier persistence.xml

<?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="monUniteDePersistance">
    <!-- la liste des noms complets des classes représentant 
         les entités gérées par cette unité de persistance  -->
    <class>ma.classe.Entite</class>
    <properties>
      <!-- une propriété de configuration propre à l'implémentation de JPA -->
      <property name="une propriété" value="une valeur" />
    </properties>
  </persistence-unit>
</persistence>

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.

Exemple de persistence.xml pour une utilisation d'OpenJPA avec une base MySQL

<?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="individuPersistenceUnit">
    <class>fr.epsi.individu.Individu</class>
    <class>fr.epsi.individu.Abonnement</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/test" />
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
      <property name="javax.persistence.jdbc.user" value="root" />
      <property name="javax.persistence.jdbc.password" value="root" />
      <property name="openjpa.jdbc.DBDictionary" value="mysql" />
      <!-- Cette propriété active la log des requêtes SQL réalisées par OpenJPA -->
      <property name="openjpa.Log" value="SQL=Trace" />
    </properties>
  </persistence-unit>
</persistence>

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.

Exemple d'initialisation de JPA

 // on spécifie le nom de l'unité de persistence en paramètre
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("individuPersistenceUnit");
 
 EntityManager entityManager = emf.createEntityManager();

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.


 EntityManager entityManager = emf.createEntityManager();
 try {

   // ...

 }
 finally {
   entityManager.close();
 }

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.

Exemple de persistence.xml pour une utilisation d'OpenJPA avec une base MySQL

<?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="individuPersistenceUnit">
    <jta-data-source>individuDataSource</jta-data-source>
    <class>fr.epsi.individu.Individu</class>
    <class>fr.epsi.individu.Abonnement</class>
    <properties>
      <property name="openjpa.jdbc.DBDictionary" value="mysql" />
      <!-- Cette propriété active la log des requêtes SQL réalisées par OpenJPA -->
      <property name="openjpa.Log" value="SQL=Trace" />
    </properties>
  </persistence-unit>
</persistence>

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 :

Injection d'une EntityManagerFactory dans un composant Java EE

import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;

@WebServlet("/MyServlet")
public class MyServlet extends HttpServlet {

  @PersistenceUnit(unitName="individuPersistenceUnit")
  private EntityManagerFactory entityManagerFactory;

}

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.

Injection d'un EntityManager dans un EJB

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ejb.Stateless;

@Stateless
public class IndividuRepository {
	
  @PersistenceContext(unitName="individuPersistenceUnit")
  private EntityManager entityManager;

}

Injection d'un EntityManager dans un composant Java EE géré par CDI

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 IndividuController {
	
  @PersistenceContext(unitName="individuPersistenceUnit")
  private EntityManager 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

Activation du support JTA dans une DataSource TomEE

<?xml version="1.0" encoding="UTF-8"?>
<tomee>
<Resource id="nomDeLaDataSource" type="javax.sql.DataSource">
  JdbcDriver com.mysql.jdbc.Driver
  JdbcUrl jdbc:mysql://localhost:3306/myDataBase
  UserName root
  Password root
  JtaManaged true
</Resource>
</tomee>

Pour signaler la démarcation transactionnelle, il existe deux solutions :

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.

Utilisation de l'API JTA dans un composant Java EE

import java.io.IOException;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.UserTransaction;

@WebServlet("/myServlet")
public class MyServlet extends HttpServlet {

  @Resource
  private UserTransaction userTransaction;

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                                  throws ServletException, IOException {

    boolean transactionOk = false;
    try {
      userTransaction.begin();

      // ...

      transactionOk = true;
    } catch (Exception e) {
      // ...
    } finally {
      try {
        if (transactionOk) {
          userTransaction.commit();
        } else {
          userTransaction.rollback();
        }
      } catch (Exception e) {
        // ...
      }
    }
  }

}

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 :

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 :

Déclaration du plugin d'amélioration dans Maven

  <plugin>
    <groupId>org.apache.openjpa</groupId>
    <artifactId>openjpa-maven-plugin</artifactId>
    <version>2.3.0</version>
    <configuration>
      <includes>**/*.class</includes>
      <addDefaultConstructor>true</addDefaultConstructor>
      <enforcePropertyRestrictions>true</enforcePropertyRestrictions>
    </configuration>
    <executions>
      <execution>
        <id>enhancer</id>
        <phase>process-classes</phase>
        <goals>
          <goal>enhance</goal>
        </goals>
      </execution>
    </executions>
  </plugin>

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 :

Activation de l'amélioration par héritage dans le fichier persistence.xml

  <property name="openjpa.RuntimeUnenhancedClasses" value="supported"/>