EJB3.1 - La persistance en Java

Chapitres traités   

Au sein d'une architecture Java EE, les EJB sont utilisés pour créer les services. Cette technologie ne s'arrête cependant pas à cette couche, mais permet aussi de créer l'abstraction de l'accès aux données. Ce sont les entités qui remplissent cette fonction.

Tout comme les beans sessions, entre le client et la logique métier, les beans entités forment la passerelle entre la logique applicative et les sources de données. Ils offrent une abstraction quasi complète du stockage des données, permettant à l'application de rendre persistantes ou de charger des données de manière totalement transparente.

Choix du chapitre Notion de persistance

Les applications d'entreprise sont composées d'une logique métier, d'interactions avec d'autres systèmes, d'interfaces utilisateur et ... de persistance. La plupart des données manipulées par les applications doivent être stockées dans des bases de données afin de pouvoir être ensuite récupérées et analysées.

Les bases de données sont importantes :

  1. Elles stockent les données métier.
  2. Elles servent de point central entre les applications.
  3. Elles traitent les données via des triggers ou des procédures stockées.

Les bases de données relationnelles

Les données persistantes sont omniprésentes - la plupart du temps, elles utilisent les bases de données relationnelles comme moteur sous-jacent. Dans un système de gestion de base de données relationnelles (SGBD), les données sont organisées en tables formées de lignes et de colonnes ; elles sont identifiées par des clés primaires (des colonnes spéciales ne contenant que des valeurs uniques) et, parfois, par des index. Les relations entre tables utilisent les clés étrangères et joignent les tables en respectant des contraintes d'intégrité.

Pour en savoir plus sur les SGBD.

Langage orienté objet et persistance

Tout ce vocabulaire est totalement étranger à un langage orienté objet comme Java. En Java, nous manipulons des objets qui sont des instances de classes ; les objets héritent les uns des autres, peuvent utiliser des collections d'autres objets et, parfois, se désignent eux-mêmes de façon récursive. Nous disposons de classes concrètes, de classes abstraites, d'interfaces, d'énumérations, d'annotations, de méthodes, d'attributs, etc.

Cependant, bien que les objets encapsulent soigneusement leur état et leur comportement, cet état n'est accessible que lorsque la machine virtuelle (JVM) s'exécute : lorsqu'elle s'arrête ou que le ramasse-miettes nettoie la mémoire, tout disparaît. Ceci dit, certains objets n'ont pas besoin d'être persistants.

Par données persistantes, nous désignons les données qui sont délibérément stockées de façon permanente sur un support magnétique, une mémoire flash, etc. Un objet est persistant s'il peut stocker son état afin de pouvoir le réutiliser par la suite.

La sérialisation

Il existe différent moyens de faire persister l'état en Java. L'un d'eux consiste à utiliser le mécanisme de sérialisation qui consiste à convertir un objet en une suite de bits : nous pouvons ainsi sérialiser les objets sur disque, sur une connexion réseau (notamment Internet), sous un format indépendant des systèmes d'exploitation. Java fournit un mécanisme simple, transparent et standard de sérialisation des objets via l'implémentation de l'interface java.io.Serializable. Cependant, bien qu'elle soit très simple, cette technique est assez fruste : elle ne fournit ni le langage d'interrogation ni support des accès concurrents intensifs ou de mise en cluster.

Pour en savoir plus sur la sérialisation.
.

JDBC

Un autre moyen de mémoriser l'état consiste à utiliser JDBC (Java Database Connectivity), qui est l'API standard pour accéder aux bases de données relationnelles. Nous pouvons ainsi nous connecter à une base et exécuter des requêtes SQL (Structured Query Language) pour récupérer un résultat. Cette API fait partie de la plate-forme Java depuis la version 1.1 mais, bien qu'elle soit toujours très utilisée, elle a tendance à être désormais éclipsée par les outils de correspondance entre modèle objet et modèle relationnel (ORM, Object-Relational Mapping), plus puissant.

Pour en savoir plus sur JDBC.

ORM

Le principe d'un ORM consiste à déléguer l'accès aux bases de données relationnelles à des outils ou des frameworks externes qui produisent une vue orientée objet des données relationnelles et vice versa. Ces outils établissent donc une correspondance bidirectionnelle entre la base et les objets.

Différents frameworks fournissent ce service, notamment Hibernate, TopLink et Java Data Objects (JDO), mais il est préférable d'utiliser JPA (Java Persistence API) car elle est directement intégrée à Java EE 6.

Spécification JPA

JPA 1.0 a été créée avec Java EE 5 pour résoudre le problème de la persistance des données en reliant les modèles objets et relationnels. Avec Java EE 6, JPA 2.0 conserve la simplicité et la robustesse de la version précédente tout en lui ajoutant de nouvelles fonctionnalités. Grâce à cette API, vous pouvez accéder à des données relationnelles et les manipuler à partir des EJB (Entreprise Java Beans), des composants web ou même des applications Java SE.

JPA est une couche d'abstraction au-dessus de JDBC, qui fournit une indépendance vis-à-vis de SQL. Toutes les classes et annotations de cette API se trouvent dans le paquetage javax.persistence.

Ses composants principaux sont les suivants :

  1. ORM, qui est le mécanisme permettant de faire correspondre les objets à des données stockées dans une base de données relationnelle.
  2. Une API gestionaire d'entités permettant d'effectuer des opérations sur la base de données, notamment les opérations CRUD (Create, Read, Update, Delete). Grâce à elle, il n'est plus nécessaire d'utiliser directement JDBC.
  3. JPQL (Java Persistence Query Language) qui permet de récupérer des données à l'aide d'un langage de requêtes orienté objet.
  4. Des mécanisme de transaction et de verrouillage lorsque nous accédons de façon concurrente aux données, fournis par JTA (Java Transaction API). Les transactions locales à la ressource (non JTA) sont également reconnues par JPA.
  5. Des fonctions de rappel et des écouteurs permettant d'ajouter la logique métier au cycle de vie d'un objet persistant.

Nouveautés de JPA 2.0

Cette seconde version ajoute de nouvelles API, étend JPQL et intègre de nouvelles fonctionnalités :

  1. Les collections de types simples (String, Integer, etc.) et d'objet intégrables (embeddable) peuvent désormais être associées à des tables distinctes alors qu'auparavant nous ne pouvions associer que des collections d'entités.
  2. Les clés et les valeurs des associations peuvent désormais être de n'importe quel type de base, des entités ou des objets intégrables.
  3. L'annotation @OrderColumn permet maintenant d'avoir un tri persistant.
  4. La suppression des orphelins permet de supprimer les objets fils d'une relation lorsque l'objet parent est supprimé.
  5. Le verrouillage pessimiste a été ajouté au verrouillage optimiste, qui existait déja.
  6. Une toute nouvelle API de définition de requêtes a été ajoutée afin de pouvoir construire des requêtes selon une approche orientée objet.
  7. La syntaxe JPQL a été enrichie (elle autorise désormais les expressions case, par exemple).
  8. Les objets intégrables peuvent maintenant être embarquées dans d'autres objets intégrables et avoir des relations avec les entités.
  9. La notation pointée a été étendue afin de pouvoir gérer les objets intégrables avec des relations ainsi que les objets intégrables d'objets intégrables.
  10. Le support d'une nouvelle API de mise en cache a été ajouté.

L'implémentation de référence de JPA est EclipseLink, anciennement TopLink d'Oracle. Ce framework est à la fois souple et puissant. Il supporte la persistance XML au travers de JAXB (Java XML Binding). Il est également désigné sous les termes de fournisseur de persistance ou, simplement, de fournisseur. Il offre un ORM, un OXM (Object XML Mapping) et la persistance des objets sur EIS (Enterprise Information Systems) à l'aide de JCA (Java EE Connector Architecture).

 

Choix du chapitre Rôle des beans entités

Les beans entités ont été créés pour simplifier la gestion des données au niveau d'une application, mais aussi pour faciliter la sauvegarde en base de données. Plus concrètement, ces beans entités vous permettent de prendre en charge la persistance des données de votre application dans une ou plusieurs des sources de données, tout en gardant les relations entre celles-ci.

Ces composants établissent donc la relation entre votre application et vos bases de données. Contrairement aux beans sessions, les données d'un bean entité sont conservées, même après l'arrêt de l'application.

Les données de l'application sont typiquement : des utilisateurs, des factures, des produits, des adresses, des informations sur une image, ... Dans le monde de Java, et plus généralement dans le monde objet, il est commun d'utiliser des classes pour chacun des types d'objets utilisés. On parle souvent d'objet métier ou entité (Entity) pour représenter les caractéristiques de ces objets.

La liaison entre les données et l'application par un objet s'appelle le mapping (relier). On parle de mapping Objet/Relationnel lorsque nous nous connectons, de cette manière, une base de donnée avec une application objet.

Attention : l'utilisation des beans entités permet de représenter une entité de l'application et non une fonctionnalité. Par exemple, Photo est un bean entité, mais StockerPhoto serait plutôt un bean session ; une photo étant vouée à rester persistante (garder un état particulier pendant un bon moment), alors que le stockage effectif de la photo est une opération, qui par définition est de courte durée.

Contrairement au bean session, les données du bean entité ont donc généralement une durée de vie longue ; elles sont enregistrées et stockées dans des systèmes de persistance (base de données).

Comprendre les entités

Lorsque nous évoquons l'association d'objets à une base de données relationnelle, la persistance des objets ou les requêtes adressées aux objets, il est préférable d'utiliser le terme d'entités plutôt que celui d'objets.

Ces derniers sont des instances qui existent en mémoire ; les entités sont des objets qui ont une durée de vie limitée en mémoire et qui par contre persistent (longuement) dans une base de données. Les entités peuvent être associées à une base de données, être concrètes ou abstraites, elles disposent de l'héritage, peuvent être mise en relation, etc.

Le principe d'un ORM consiste à déléguer à des outils ou des frameworks externes (JPA, dans notre cas) la création d'une correspondance entre les objets et les tables. Le monde des classes, des objets et des attributs peut alors être associé aux bases de données constituées de tables formées de lignes et de colonnes. Cette association offre une vue orientée objet aux développeurs, qui peuvent alors utiliser de façon transparente des entités à la place des tables.

Propriétés d'un bean entité

Ainsi, l'objet à rendre persistant, via un mapping Objet/Relationnel, correspond à une table de la base de données.

Chaque propriété de cet objet est liée à un champ de la table.
Chaque instance de cet objet représente généralement un enregistrement (une ligne) de la table.

  1. Les beans entités suivent le même principe et doivent tous posséder un identifiant unique (clé primaire). Une bean entité Banque, par exemple, peut être identifié à partir de son numéro de compte numéroCompte. Cet identifiant unique permet alors à l'application de retrouver les données du bean entité associé.
  2. Un bean entité Utilisateur est, par exemple, caractérisé par des propriétés telles que : nom, prénom, email, téléphone, etc. Nous appelons ces propriétés : champs persistants. Ces propriétés sont mappées (liées) aux champs de la table associée. Durant l'exécution du programme, le conteneur d'EJB synchronise automatiquement l'état de ces propriétés avec la base de données.
  3. Comme une table, un bean entité possède des champs relationnels. Toutefois, une grande différence existe. Un champ relationnel est représenté, dans un bean entité, par une propriété dont le type est un autre bean entité ce qui correspond à l'agrégation dans la programmation objet. A l'opposé, une table est liée à une autre table par une clé étrangère.

Exemple avec le bean entité Photo

Après toutes ces considérations techniques, je vous propose de voir comment créer notre propre bean entité Photo qui va permettre de rendre persistant un certain nombre d'informations relatives au stockage d'une photo. Dans le code suivant, vous remarquez que sur notre entité Photo, quelques attributs sont annotés (id, instant) alors que d'autres ne le sont pas.

Photo.java
import java.awt.image.BufferedImage;
import java.util.Date;
import javax.persistence.*;

@Entity
public class Photo  {
    @Id
    private String id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date instant;
    @Column(nullable = false)
    private int largeur;
    @Column(nullable = false)
    private int hauteur;
    private long poids;

    public String getId()            { return id; }
    public int      getHauteur()   { return hauteur;  }
    public Date   getInstant()     { return instant;  }
    public int      getLargeur()   { return largeur;  }
    public long   getPoids()       { return poids;  }

    public Photo() { }

    public Photo(String nom, long poids) {
        id = nom;
        instant = new Date();
        this.poids = poids;
    }

    public void setDimensions(BufferedImage image) {
        largeur = image.getWidth();
        hauteur = image.getHeight();
    }

    @Override
    public String toString() {
        return id+" ("+largeur+", "+hauteur+")";
    }
}
  1. Pour être reconnue comme entité, la classe Photo doit être annotée par @javax.persistence.Entity.
  2. L'annotation @javax.persistance.Id sert à indiquer la clé primaire, et la valeur de cet identifiant peut éventuellement être généré automatiquement par le fournisseur de persistance. Pour cela vous devez rajouter l'annotation @GeneratedValue :
    @Entity
    public class Photo  {
        @Id  @GeneratedValue
        private String id;
    ...
    }
  3. L'annotation @Temporal permet de préciser qu'elle type de date nous souhaitons utiliser au niveau de la table de la base de données.
  4. L'annotation @Column est utilisé avec certains attributs pour adapter la correspondance par défaut des colonnes (largeur et hauteur ne peuvent plus contenir NULL).
  5. Mis à part toutes ces petites annotations, l'entité Photo est une classe comme une autre. A ce titre, nous pouvons rajouter toutes les méthodes qui nous paraîssent nécessaires pour une meilleure utilisation possible. Par exemple, le deuxième constructeur de Photo nous permet en une seule fois de gérer respectivement les attributs id, nom et poids alors que setDimensions() permet de compléter les derniers attributs restants.
  6. Le fournisseur de persistance pourra ainsi faire correspondre l'entité Photo à une table PHOTO (règle de correspondance par défaut), produire une clé primaire et synchronyser les valeurs des attributs vers les colonnes de la table :


Interrogation des entités

JPA permet de faire correspondre les entités à des bases de données et de les interroger en utilisant différents critères. La puissance de cette API vient du fait qu'elle offre la possibilité d'interroger les entités et leurs relations de façon orientée objet sans devoir utiliser les clés étrangères ou les colonnes de la base de données sous-jacentes.

L'élément central de l'API, responsable de l'orchestration des entités, est le gestionnaire d'entité : son rôle consiste à gérer les entités, à lire et à écrire dans une base de données et à autoriser les opérations CRUD simples sur les entités, ainsi que des requêtes complexes avec JPQL.

D'un point de vue technique, le gestionnaire d'entité n'est qu'une interface dont l'implémentation est donnée par le fournisseur de persistance, EclipseLink. Le code suivant montre comment créer un gestionnaire d'entité et rendre l'entité Photo persistante au moyen de la méthode stocker() du bean session Archiver :

Archiver.java
@Stateless
public class Archiver  {
    private final String répertoire = "E:/Archivage/";
    @PersistenceContext (unitName="Photos-ejbPU") // s'il existe une seule unité de persistance, la désignation de son nom n'est pas obligatoire
    EntityManager persistance;

    public void stocker(String nom, byte[] octets) throws IOException  {
        File fichier = new File(répertoire+nom);
        if (fichier.exists()) return;
        FileOutputStream flux = new FileOutputStream(fichier);
        flux.write(octets);
        flux.close();
        BufferedImage image = ImageIO.read(fichier);
        Photo photo = new Photo(nom, fichier.length());
        photo.setDimensions(image);
        persistance.persist(photo);
    }
...
}

La figure suivante montre comment l'interface EntityManager peut être utilisée par notre bean session Archiver pour manipuler l'entité Photo :

Voici une autre portion de code qui montre comment créer l'unité de persistence Photos-ejbPU, au travers cette fois-ci d'une simple application Java SE (standalone) sans passer par un bean session intermédiaire :

Visionneuse.java
 11 import javax.persistence.*;
 12 
 13 public class Visionneuse extends JFrame implements ActionListener {
 14    private static FichiersPhotoRemote fichiers; 
 15    private static EntityManagerFactory fabrique;
 16    private static EntityManager persistance;
...
 25    private Photo photo; 
... 
 80    public static void main(String[] args) throws Exception {
 81        Context ctx = new InitialContext();
 82        fichiers = (FichiersPhotoRemote) ctx.lookup(photos.FichiersPhotoRemote.class.getName());
 83        fabrique = Persistence.createEntityManagerFactory("Photos-ejbPU");
 84        persistance = fabrique.createEntityManager();
 85        persistance.getTransaction().begin();
 86        new Visionneuse();
 87    }
...

Le gestionnaire d'entités permet également d'interroger les entités. Dans ce cas, une requête JPA est semblable à une requête sur une base de données, sauf qu'elle utilise JPQL au lieu de SQL. La syntaxe utilise la notation pointée habituelle. Pour récupérer, par exemple toutes les photos enregistrées, il suffirait d'écrire :

SELECT photo FROM Photo AS photo

Une instruction JPQL peut exécuter des requêtes dynamiques (créées à l'exécution), des requêtes statiques (définies lors de la compilation), voire des instructions SQL natives. Les requêtes statiques, également appelées requêtes nommées, sont définies par des annotations directement sur le bean entité.

L'instruction JPQL précédente peut, par exemple, être définie comme une requête nommée sur l'entité Photo :

Photo.java
@Entity
@NamedQuery(name="toutes", query="SELECT photo FROM Photo AS photo")
public class Photo  {
    @Id
    private String id;
...
}

A l'aide de la méthode createNamedQuery() de EntityManager nous permet d'exécuter la requête nommée et nous renvoie ainsi la liste des photos correspondant aux critères de recherche, c'est-à-dire toutes les photos stockées :

Archiver.java
@Stateless
public class Archiver  {
    private final String répertoire = "E:/Archivage/";
    @PersistenceContext
    EntityManager persistance;
...
    public List<Photo> getListe() { return persistance.createNamedQuery("toutes").getResultList(); }

    public Photo getPhoto(String nom) {  return persistance.find(Photo.class, nom);   }

    public void supprimer(String nom) {
        new File(répertoire+nom).delete();
        Photo photo = getPhoto(nom);
        persistance.remove(photo);
    }
}

Méthode de rappel et écouteur

Les entités sont simplement des POJO (Plain Old Java Objects) qui sont gérés ou non par le gestionnaire d'entités. Lorsqu'elles sont gérées, elles ont une identité de persistance et leur état est synchronisé avec la base de données. Lorsqu'elles ne le sont pas (elles sont, par exemple, détachées du gestionnaire d'entités), elles peuvent être utilisées comme n'importe quelle autre classe Java : ce qui signifie que les entités ont un cycle de vie.

Lorsque vous créez une instance de l'entité Photo à l'aide de l'opérateur new, l'objet existe en mémoire et JPA ne le connaît pas (il peut même finir par être supprimé par le ramasse-miettes) ; lorsqu'il devient géré par le gestionnaire d'entités, son état est associé et synchronisé aves la table PHOTO. L'appel de la méthode remove() de EntityManager supprime les données de la base, mais l'objet Java continue d'exister en mémoire jusqu'à ce que le ramasse-miettes le détruise.


Les opérations qui s'appliquent aux entités peuvent se classer en quatre catégories : persistance, mis à jour, suppression et chargement qui correspondent respectivement aux opérations d'insertion, de mis à jour, de suppression et de sélection dans la base de données.

Chaque opération possède un événement "Pre" et "Post" (sauf le chargement, qui n'a qu'un événement "Post") qui peuvent être interceptés par le gestionnaire d'entités pour invoquer dès lors une méthode métier. Comme nous le découvrirons dans une autre étude, il existe donc des annotations @PrePersist, @PostPersist, etc. Ces annotations peuvent être associées à des méthodes d'entités (appelées fonctions de rappel) ou à des classes externes (appelées écouteurs). Vous pouvez considérer les fonctions de rappel et les écouteurs comme des triggers d'une base de données relationnelle.

 

Choix du chapitre Mise en oeuvre de l'unité de persistance

Partie intégrante du framework Java EE 6, l'unité de persistance (JPA) est la boîte noire qui permet de rendre persistants les beans entités. Plus qu'un simple fournisseur de persistance, celle-ci va permettre aux développeurs d'optimiser leurs applications selon la gestion de leurs beans entités.

L'unité de persistance est l'élément clé de la gestion des beans entités au sein d'une application. Effectivement, les beans entités étant des objets simples, ils doivent être managés par une unité de persistance qui permet d'intégrer l'utilisation de ces beans entités au sein d'applications Java EE et même au travers de Java SE (sans passer par un bean session).

Qu'est-ce qu'une unité de persistance ?

Bien que la mise en place des beans entités EJB 3.1 au sein d'une application soit assez simple, la persistance des informations doit être configurée pour permettre la sauvegarde des données dans la ou les source(s) de données.

La solution adoptée pour la gestion de ces beans entités est l'utilisation d'une unité de persistance (ou contexte de persistance) qui prend en charge la sauvegarde des informations dans la source de données, de manière autonome (indépendamment des beans entités).

Une unité de persistance est caractérisée par les points suivants :

  1. Un ensemble de beans entités.
  2. Un fournisseur de persistance (Provider).
  3. Une source de données (Datasource).

Une unité de persistance étant vouée à être enregistrée dans une source de données, le rôle de l'unité de persistance est :

  1. De savoir où et comment stocker les informations.
  2. De s'assurer de l'unicité des objets de chaque entité persistante.
  3. De gérer les objets et leur cycle de vie : c'est le gestionnaire d'entité qui s'en occupe (EntityManager).

Il est possible d'avoir un bean entité attaché ou détaché de l'unité de persistance. Quand un bean entité est attaché à un contexte de persistance, les modifications appliquées à l'objet sont alors automatiquement synchronisées avec la base de donnés, via le gestionnaire d'entité (EntityManager). A l'inverse, un bean entité est dit détaché lorsqu'il n'a aucun lien avec le gestionnaire d'entité (EntityManager).

Intégration et packaging d'une unité de persistance

Auparavant, les beans entités se cantonnaient uniquement au sein de Java EE. Depuis la version EJB 3, l'utilisation des beans entités n'est plus fermée au monde Java EE ou à un conteneur EJB. Vous pouvez désormais déployer vos entités dans de nombreuses applications :

  1. Application d'entreprise (EAR).
  2. Module Java Bean Entreprise (EJB-JAR).
  3. Application Web (WAR).
  4. Client d'application entreprise (JAR)
  5. Un environnement Java SE compatible.

Ce dernier point est, sans doute, le plus appréciable. En effet, il vous permet d'inclure facilement et simplement un système de persistance de données à une application Java SE.

Paramétrage de l'unité de persistance

La nouvelle spécification définit un fichier de description qui regroupe l'ensemble des informations de persistance. Ce fichier décrit les beans entités qui vous seront utiles pour l'application, l'emplacement de la base de données, ainsi que le fournisseur de persistance qui correspond généralement au serveur d'applications.

Ce fichier sera alors lu par le conteneur d'EJB lors du déploiement de l'application (EAR, EJB-JAR, WAR, ...). Au moment du déploiement le conteneur vérifie la description proposée et contrôle la concordance avec les éléments qu'il connaît : les beans entités sont-ils présents dans le serveur d'application ? la base de données est-elle accessible ? etc. Si tout se passe bien, le conteneur crée alors une instance de persistance avec les paramètres demandés. Cette unité de persistance sera ainsi toujours opérationnelle et active à tout instant.

Vous devez nommer ce fichier de description de persistance : persistence.xml et placer ce dernier dans le répertoire <META-INF> à la racine du projet.

Attention : vous devez nommer correctement le fichier persistence.xml. Si le nom ne correspond pas, le conteneur n'associera aucun contexte de persistance à l'application.

Voici deux exemples de fichier de description de persistance qui seront d'ailleurs utilisés dans les chapitres qui suivent :

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="PhotoListe-ejbPU" transaction-type="JTA">
       <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
       <jta-data-source>photo</jta-data-source>
       <properties>
           <property name="eclipselink.ddl-generation" value="create-tables"/>
       </properties>
   </persistence-unit>
</persistence>
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
      
  <persistence-unit name="Photos-ejbPU" transaction-type="RESOURCE_LOCAL">
      <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
      <class>photos.Photo</class>
      <properties>
         <property name="eclipselink.jdbc.user" value="manu"/>
         <property name="eclipselink.jdbc.password" value="manu"/>
         <property name="eclipselink.jdbc.url" value="jdbc:derby://localhost:1527/photos"/>
         <property name="eclipselink.target-database" value="DERBY"/>
         <property name="eclipselink.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver"/>
         <property name="eclipselink.ddl-generation" value="drop-and-create-table" />
         <property name="eclipselink.logging.level" value="INFO"/>
      </properties>
   </persistence-unit>
</persistence>
  

Ce fichier est un document XML. Comme tout document XML, il doit respecter un certain canevas. Ici, l'élément racine est la balise <persistence>. La balise racine ne contient ensuite qu'une ou plusieurs balises <persistence-unit>. Cette dernière décrit comment mettre en place une unité de persistance. Il est ainsi possible d'avoir plusieurs unités de persistance dans le cas, par exemple, où notre application utilise plusieurs bases de données (une unité de persistance par base de données).

<persistence-unit> : déclare une unité de persistance.
- L'attribut name affecte un nom unique à cette unité dans votre application. Le nom est utilisé pour identifier l'unité de persistance lors de son utilisation avec les annotations @PersistenceContext et @PersistenceUnit pour la création, respectivement, d'un EntityManager ou d'un EntityManagerFactory (voir plus loin dans notre étude).
- L'attribut type définit si l'unité de persistance est gérée et intégrée dans une transaction Java EE (JTA) ou si vous souhaitez gérer de façon manuelle les transactions (RESOURCE_LOCAL) via l'EntityManager (ou l'EntityManagerFactory). La valeur par défaut en environnement Java EE est JTA et RESOURCE_LOCAL en environnement Java SE. Des détails concernant ce type sont donnés plus loin.

Je le rappelle, pensez bien qu'il est nécessaire de définir plusieurs unités de persistance si vous souhaitez utiliser plusieurs sources de données.

Voici maintenant une description des balises qui spécifie les paramètres de l'unité de persistance. Vous devez donc placer ces balises à l'intérieur d'une balise <persistence-unit> :
  1. <description> : permet de décrire l'unité de persistance obligatoire. Il s'agit juste d'une information.
  2. <provider> : permet de définir la classe d'implémentation du fournisseur de persistance utilisé dans l'application (sous-classe de javax.persistence.spi.PersistenceProvider). Chaque unité de persistance utilise un fournisseur unique. Vous pouvez cependant, en environnement Java EE, utiliser le fournisseur par défaut du serveur d'applications :
    --- JBoss utilise Hibernate : org.hibernate.ejb.HibernatePersistence
    --- Oracle Application Server utilise TopLink : oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
    --- GlassFish utilise également TopLink : oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
    --- KODO : kodo.persistence.PersistenceProviderImpl
    --- En environnement Java SE, vous devez spécifier le fournisseur de persistance.
  3. <jta-datasource> : permet de définir le nom JNDI de la source de données transactionnelle à utiliser. Celle-ci doit être paramétrée sur le serveur et prendre en compte les transactions.
  4. <non-jta-datasource> : permet de définir les noms JNDI de la source de données non transactionnelle à utiliser. Celle-ci doit être paramétrée sur le serveur. En environnement Java SE, vous ne pouvez pas utiliser de source de données. Il faut utiliser les propriétés (balise <properties>) du provider pour spécifier la base de données à utiliser.
  5. <mapping-file> : Cette balise permet de définir le fichier de mapping XML pour les beans entités (lorsqu'il n'est pas possible d'utiliser les annotations).
  6. <properties> : Cette balise permet de configurer les attributs de configuration du fournisseur de persistance. Cette balise regroupe l'ensemble des propriétés <property>. Une <property> est définie par un nom (attribut name> et une valeur (attribut value).

Une unité de persistance mappe un ensemble de beans entités. Par défaut, dans un environnement Java EE, le conteneur analyse l'ensemble des classes du fichier *.jar contenant le fichier persistence.xml. Pour définir manuellement les classes à mapper, vous pouvez utiliser les balises suivantes :

  1. <jar-file> : permet de définir le(s) fichier(s) JAR contenant les classes beans entités à analyser.
  2. <class> : permet de définir les noms des classes à intégrer dans l'unité. En environnement Java SE, vous devez définir l'ensemble des classes d'entités.
  3. <exclude-unisted-classes> : indique que les classes non listées explicitement ne doivent pas être intégrées à cette unité. En environnement Java EE, le conteneur cherche automatiquement les classes annatées @Entity et les ajoute aux unités de persistance. Vous devez utiliser la balise <exclude-unisted-classes> lorsque plusieurs unités de persistances sont définies et qu'elles utilisent des ensembles distincts de beans entités dans une même application.

Finalement, l'ensemble des classes à mapper est défini par l'ensemble des :

  1. Classes annotées avec @Entity (bean entité) incluses dans le fichier JAR définis via <jar-file>.
  2. Classes listées via <class>.

Généralement, vous n'avez pas à utiliser les balises <jar-file> et <class> sauf si vous souhaitez mapper une classe à plusieurs unités de persistance.

Le fait de proposer un fichier de description est un très gros atout. Il vous est possible, à tout moment, de changer votre serveur de base de données ou même de serveur d'applications, sans que vous ayez à recompiler et à reconstruire votre application d'entreprise. Ce fichier sert ainsi de fichier de configuration.

 

Choix du chapitre Projet de stockage et de visualisation des photos à distance

Après avoir découvert le rôle d'un bean entité et comment le rendre persistant, nous allons mettre en pratique ces différentes définitions. Ainsi, nous verrons comment construire un bean entité et comment le gérer au travers de l'unité de persistance. L'unité de persistance, elle-même, sera mis en oeuvre au travers d'un bean session.

Afin de visualiser l'intérêt d'utiliser des beans entités, je vous propose de faire la démonstration au travers d'un projet. Ce projet consiste à réaliser un tout petit serveur de photos qui me permet de stocker un ensemble réduit de clichés numériques qui seront bien sûr possibles de consulter par la suite.

  1. Ainsi, une première application cliente s'occupe de visualiser l'ensemble des photos prises et présentes dans un des répertoires du poste local. Après avoir identifiées la ou les photos à archiver, l'utilisateur envoie les fichiers correspondants au serveur.
  2. Cette même application cliente, peut ensuite consulter et visualiser les photos qui sont archivées avec en plus un certain nombre de paramètres qui peuvent s'avérer utiles comme les dimensions de la photo par exemple.


Dans notre exemple, la base de données se situe avec le serveur d'application Java EE. Elle peut se situer, bien entendu, sur une machine distincte. Le fonctionnement demeure totalement identique.

Création de la base de données photos

Vous allez le découvrir, la gestion des bases de données avec les beans entités sont très simples à mettre en oeuvre puisque les tables sont automatiquement créées au moment même où vous déployez votre application d'entreprise (contenant des beans entités) dans votre serveur d'applications. Nous devons quand même réaliser une petite opération, c'est effectivement de créer la base de données, qui va accueillir les différentes tables prévues par l'application d'entreprise, référencée par le service de nommage JNDI.

Je vous propose de voir comment créer cette base de données en utilisant le serveur d'applications Glassfish avec sa base de données interne Derby au moyen de l'environnement NetBeans.

Nous aurions pu attendre avant de mettre en place cette base de données. Effectivement, nous aurions pu créer la base de données juste au moment de la mise en oeuvre du bean entité. Le système vérifie si la base de données existe et propose de la construire le cas échéant.

Création d'un projet d'entreprise

Nous allons maintenant constituer notre projet dans sa globalité, d'une part la gestion complète côté serveur d'applications et d'autre part l'application cliente qui va utiliser ce service. Nous l'avons déjà largement évoqué lors de l'étude précédente, l'idéal est de constituer un projet d'entreprise avec donc les deux modules :

  1. Un module EJB : constitué d'un bean entité qui permet la persistance des informations relatives aux photos stockées, et d'un bean session qui s'occupe de l'enregistrement physique des différentes photos sur le disque dur du serveur et qui intègre le gestionnaire d'entités pour gérer correctement la persistance.
  2. Un module client : intégré dans un conteneur ACC qui s'occupe de récupérer les photos sur le disque local afin de les archiver sur le serveur afin de pouvoir les récupérer par la suite.

Création du bean entité et de l'unité de persistance

Nous allons maintenant nous occuper de la persisance dans sa globalité, d'une part l'entité représentant les informations importantes à sauvegarder pour chacune des photos stockées et d'autre part l'unité de persistance qui s'occupera de réaliser le mapping objet/relationnel.

Voici la procédure à suivre dans l'environnement de Netbeans :

  1. Dans le module EJB, demande de création d'une nouvelle entité :

  2. Définition de la nouvelle entité avec création automatique de l'unité de persistance correspondante :


  3. Définition de l'unité de persistance et demande de création du nom JNDI associé à la base de données créée précédemment :


  4. Création du nom JNDI associé à la base de données photos :


  5. Et pour terminer validation de l'association entre l'unité de persistance et le nom JNDI de la base de données photos :


L'entité Photo est maintenant constituée avec un codage par défaut qu'il suffit de modifier en conséquence. Remarquez au passage que le fichier de configuration persistence.xml a été automatiquement généré lors de la phase précédente :

entité.Photo.java
package entité;

import java.awt.image.BufferedImage;
import java.util.Date;
import javax.persistence.*;

@Entity
@NamedQuery(name="toutes", query="SELECT photo FROM Photo AS photo")
public class Photo  implements java.io.Serializable {
    @Id
    private String id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date instant;
    private int largeur;
    private int hauteur;
    private long poids;

    public String getId()            { return id; }
    public int      getHauteur()   { return hauteur;  }
    public Date   getInstant()     { return instant;  }
    public int      getLargeur()   { return largeur;  }
    public long   getPoids()       { return poids;  }

    public Photo() { }

    public Photo(String nom, long poids) {
        id = nom;
        instant = new Date();
        this.poids = poids;
    }

    public void setDimensions(BufferedImage image) {
        largeur = image.getWidth();
        hauteur = image.getHeight();
    }

    @Override
    public String toString() {
        return id+" ("+largeur+", "+hauteur+")";
    }
}

Un point important lors de la mise en oeuvre d'une entité. Vous remarquez que Netbeans propose systématiquement que chaque entité implémente l'interface Serializable. Ce choix est parfaitement judicieux si vous désirez que cette information se propage sur le réseau afin que le client puisse en bénéficier.

Avec ce système, nous obtenons beaucoup d'indirection avant d'atteindre la base de données effective. En effet, l'unité de persistance Archivage-Photos-ejbPU fait référence au nom JNDI photos qui lui même fait référence au pool de connexion derby_net_photosPool (nom par défaut proposé par Netbeans). C'est ce dernier élément qui possède toutes les caractéristiques essentielles de la base de données : serveur utilisé, driver associé, numéro de service, nom de la base, nom d'utilisateur, etc.

Création du bean session Archiver

Toujours dans notre module EJB, nous devons maintenant implémenter le bean session qui va s'occuper de tout le traitement nécessaire côté serveur ; d'une part l'enregistrement physique des différentes photos sur le disque dur du serveur et d'autre part la gestion complète de la persistance des entités relatives aux informations supplémentaires des photos archivées.

Voici la procédure à suivre dans l'environnement de Netbeans :

  1. Dans le module EJB, demande de création d'un nouveau bean session :

  2. Définition du nouveau bean session :


Le bean session Archiver ainsi que son interface distante ArchiverRemote sont maintenant constitués avec un codage par défaut qu'il suffit de modifier en conséquence :

session.ArchiverRemote.java
package session;

import javax.ejb.Remote;
import entité.Photo;

@Remote
public interface ArchiverRemote {
    void stocker(String nom, byte[] octets) throws java.io.IOException;
    byte[] restituer(String nom) throws java.io.IOException;
    List<Photo> getListe();
    Photo getPhoto(String nom);
    void supprimer(String nom);
} 
session.Archiver.java
package session;

import java.util.List;
import javax.ejb.*;
import javax.persistence.*;
import entité.Photo;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.ImageIO;

@Stateless
public class Archiver implements ArchiverRemote  {
    private final String répertoire = "D:/Archivage/";
    @PersistenceContext
    EntityManager persistance;
    
    @Override
    public void stocker(String nom, byte[] octets) throws IOException  {
        File fichier = new File(répertoire+nom);
        if (fichier.exists()) return;
        FileOutputStream fluxphoto = new FileOutputStream(fichier);
        fluxphoto.write(octets);
        fluxphoto.close();
        enregistrer(nom);
    }

    @Asynchronous
    private void enregistrer(String nom) throws IOException {
        File fichier = new File(répertoire+nom);
        BufferedImage image = ImageIO.read(fichier);
        Photo photo = new Photo(nom, fichier.length());
        photo.setDimensions(image);
        persistance.persist(photo);
    }
    
    public byte[] restituer(String nom) throws IOException {
File fichier = new File(répertoire+nom);
if (!fichier.exists()) return null;
FileInputStream fluxphoto = new FileInputStream(fichier);
byte[] octets = new byte[(int)fichier.length()];
fluxphoto.read(octets);
fluxphoto.close();
return octets;
} @Override public List<Photo> getListe() { return persistance.createNamedQuery("toutes").getResultList(); } @Override public Photo getPhoto(String nom) { return persistance.find(Photo.class, nom); } @Override public void supprimer(String nom) { new File(répertoire+nom).delete(); Photo photo = getPhoto(nom); persistance.remove(photo); } }

Même si cela n'est pas précisé explicitement, c'est bien l'unité de peristance Archivage-Photos-ejbPU qui est utilisée puisque c'est la seule qui est définie dans le projet.

Application cliente en résau local

Nous concluons notre étude par la mise en oeuvre de l'application cliente qui pourra être déployée automatiquement depuis le serveur d'application du moment que nous restons dans le réseau local de l'entreprise (le numéro de service 3700 pour les objets distants de type bean session va être bloqué par le pare-feu de l'entreprise). Pour une bonne ergonomie, cette application cliente est bien entendue constituée d'une IHM dont je rappelle les fonctionnalités suivantes :

  1. L'application cliente doit s'occuper de visualiser l'ensemble des photos prises et présentes dans un des répertoires du poste local. Après avoir identifiées la ou les photos à archiver, l'utilisateur envoie les fichiers correspondants au serveur.
  2. Cette même application cliente, peut ensuite consulter et visualiser les photos qui sont archivées avec en plus un certain nombre de paramètres qui peuvent s'avérer utiles comme les dimensions de la photo.
photos.Client.java
package photos;

import java.awt.BorderLayout;
import java.awt.event.*;
import java.io.*;
import javax.ejb.EJB;
import javax.imageio.ImageIO;
import javax.swing.*;
import session.ArchiverRemote;
import entité.Photo;

public class Client extends JFrame {
    private JTabbedPane onglets = new JTabbedPane();
    private JPanel panneauLocal = new JPanel(new BorderLayout());
    private JPanel panneauServeur = new JPanel(new BorderLayout());
    private JToolBar outilsLocal = new JToolBar();
    private JToolBar outilsDistant = new JToolBar();
    private JLabel photoLocale = new JLabel();
    private JLabel photoDistante = new JLabel();
    private JLabel description = new JLabel();
    private JComboBox listePhotos = new JComboBox();
    private JFileChooser sélecteur = new JFileChooser();
    private Photo photo;
    private File fichier;
    private byte[] octets;
    private boolean effacer = true;
    @EJB
    private static ArchiverRemote archivage;

    public Client() {
        super("Envoyer des photos");
        add(onglets);
        onglets.add("Photos en local", panneauLocal);
        onglets.add("Photos distantes", panneauServeur);

        panneauLocal.add(outilsLocal, BorderLayout.NORTH);
        panneauLocal.add(new JScrollPane(photoLocale));
        outilsLocal.add(new AbstractAction("Sélectionner") {
            public void actionPerformed(ActionEvent e) {
                sélecteur.setFileSelectionMode(JFileChooser.FILES_ONLY);
                if (sélecteur.showOpenDialog(Client.this)==JFileChooser.APPROVE_OPTION) {
                    fichier = sélecteur.getSelectedFile();
                    photoLocale.setIcon(new ImageIcon(fichier.getPath()));
                }
            }
        });
        outilsLocal.add(new AbstractAction("Envoyer") {
            public void actionPerformed(ActionEvent e) {
                if (fichier!=null)
                    try {
                        byte[] octets = new byte[(int) fichier.length()];
                        FileInputStream lecture = new FileInputStream(fichier);
                        lecture.read(octets);
                        lecture.close();
                        archivage.stocker(fichier.getName(), octets);
                        listingPhotos();
                    }
                    catch (Exception ex) {
                        setTitle("Impossible d'envoyer le fichier");
                    }
              }
        });

        panneauServeur.add(outilsDistant, BorderLayout.NORTH);
        panneauServeur.add(new JScrollPane(photoDistante));
        panneauServeur.add(description, BorderLayout.SOUTH);

        outilsDistant.add(new AbstractAction("Restituer") {
            public void actionPerformed(ActionEvent e) {
                sélecteur.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
                if (sélecteur.showSaveDialog(Client.this)==JFileChooser.APPROVE_OPTION) {
                    try {
                        fichier = new File(sélecteur.getSelectedFile() + "/" +photo.getId());
                        FileOutputStream fluxImage = new FileOutputStream(fichier);
                        fluxImage.write(octets);
                        fluxImage.close();
                    } 
                    catch (Exception ex) {  setTitle("Problème pour restituer la photo en local"); }                  
                }
            }
        });
        outilsDistant.add(new AbstractAction("Supprimer") {
            public void actionPerformed(ActionEvent e) {
                archivage.supprimer(photo.getId());
                listingPhotos();
            }
        });
        outilsDistant.add(listePhotos);
        listePhotos.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!effacer)
                    try {
                        photo = (Photo) listePhotos.getSelectedItem();
                        octets = archivage.restituer(photo.getId());
                        ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets);
                        photoDistante.setIcon(new ImageIcon(ImageIO.read(fluxImage)));
                    }
                    catch (Exception ex) {  setTitle("Problème pour récupérer l'image du serveur");  }
                }
        });
        listingPhotos();
        setSize(500, 400);
        setLocationByPlatform(true);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }

    private void listingPhotos() {
        effacer = true;
        listePhotos.removeAllItems();
        for (Photo photo : archivage.getListe()) listePhotos.addItem(photo);
        effacer = false;
        if (listePhotos.getItemCount()>0) listePhotos.setSelectedIndex(0);
    }
    
    public static void main(String[] args) { new Client(); }
}
Pour conclure et pour bien maîtriser et comprendre la globalité du système, je vous propose de voir l'ensemble des diagrammes UML visualisant les différentes fonctionnalités attendues sur l'informatique répartie (client-serveur) de ce service d'archivages de photos :

Diagramme de cas d'utilisation

Diagramme de séquence de Stocker

Diagramme de séquence Restituer

Diagramme de séquence Supprimer

Résultat obtenu dans la bases de données photos