1. Introduction

Ce tutoriel va vous montrer comment intéger un modèle métier persisté par le driver JDO JPox au sein d'un serveur d'application Tomcat 5.x. Pour une explication complète des concepts JDO reportez vous à l'article Tutorial sur l'utilisation de JDO sur une base de donnée relationnelle. Ce tutorial suppose que le lecteur est familié avec le serveur d'application Tomcat 5.x et possède des connaissances sur les mécanismes de persistance JDO. La première partie de l'article est consacrée à l'installation des produits. Ensuite il enchaine sur le déploiement d'un exemple et termine sur les quelques points spécifiques à cette intégration.

2. Installation des composants

Dans notre tutorial nous allons faire persister nos classes Java dans une base de données MySQL en utilisant le driver opensource JPox s'exécutant dans le container Web Apache/Tomcat.

2.1. MySQL

2.1. JPox

2.2. Tomcat 5.x

Tomcat est une implémentation open source d'un container web J2EE. Il permet d'écrire des applications web dynamiques en utilisant les technologies Servlet et JSP. Pour télécharger une version, rendez vous ICI. Il existe pour les environnements de type Microsoft Windows, un programme d'installation automatique. Pour les autres environnements, il faut télécharger une version archive type .tar.gz.
Pour plus d'informations sur la technologie Servlet et Tomcat, reportez vous à l'article Créer et déployer un premier servlet avec TomCat publié par LFE.

3. Déploiement

Cette partie va détailler comment déployer un exemple d'un modèle géré en persistance par JDO dans le container web Tomcat. Vous aurez besoin pour ceci de télécharger l'archive (WAR) et le fichier de paramétrage.

3.1. Installation de l'application Discothèque

Cette application est la suite du modèle Complexe présenté dans la première partie. Le modèle comporte des notions d'artistes, de personnes, de groupes, d'albums, de pistes. Les services de notre application discothèque vont nous permettre de créer des instances du modèle, de vérifier que l'on a bien tout persisté et d'effectuer des requêtes.

Démarrez le serveur d'application tomcat et ouvrez la page d'administration http://localhost:8080/manager/html

Vous devez obtenir un écran comme celui-ci:

Tomcat: Console manager

Cliquez sur le bouton "Parcourir" de la section "WAR File to Deploy" et Sélectionner le fichier discotheque.war présent dans le fichier tutoriel_jpox_tomcat.zip.
Cliquez sur le bouton "Deploy"

Déploiement discotheque.war

L'application est déployée. Si vous cliquez sur le lien /discotheque vous devez arriver sur une page vous proposant les 3 services de l'applications. Ne cliquez pas tout de suite sur les liens, il reste à configurer la base de données.

3.2. Paramétrage de la source de données

Avant d'exécuter notre application, il faut définir la source de données utilisée par le driver JDO. Cette source de données sera gérée par le web container Tomcat. Si vous ouvrez le fichier discotheque.xml, vous voyez:

discotheque.xml
Sélectionnez

<Context docBase="discotheque" path="/discotheque" reloadable="true" source="com.ibm.wtp.web.server:disco">
  <Logger className="org.apache.catalina.logger.FileLogger"  prefix="localhost_discotheque_log." suffix=".txt" timestamp="true"/>
  <Resource auth="Container" name="jdbc/discoDB" type="javax.sql.DataSource"/>
  <ResourceParams name="jdbc/discoDB">
    <parameter>
      <name>factory</name>
      <value>org.apache.commons.dbcp.BasicDataSourceFactory</value>
    </parameter>
    <parameter>
      <name>username</name>
      <value>root</value>
    </parameter>
    <parameter>
      <name>password</name>
      <value/>
    </parameter>
    <parameter>
      <name>driverClassName</name>
      <value>com.mysql.jdbc.Driver</value>
    </parameter>
    <parameter>
      <name>url</name>
      <value>jdbc:mysql://localhost:3306/test?autoReconnect=true</value>
    </parameter>
  </ResourceParams>
</Context>				
				

Il vous faut modifier les paramètres de la Datasource (driverClassName, url, username et password) en adéquation avec votre configuration mysql. Ensuite, copier le fichier discotheque.xml dans le répertoire %TOMCAT_HOME%/conf/Catalina/localhost et recharger l'application avec l'aide de l'application Manager de Tomcat.

3.3. Création des données

L'application est prête à être utilisée. Lancer votre navigateur sur l'url http://localhost:8080/discotheque/. Cliquer sur "Création des Données: CreateData". Ce service va peupler la base de données. Les données insérées sont les mêmes que l'exemple 4.3. Création de données.

Si tout se passe correctement l'application vous répond:

 
Sélectionnez

Creation des données..................ok
				

3.4. Vérification des données

Pour vérifier que les données sont correctement persistées, nous allons compter le nombre d'instances insérées. Lancer votre navigateur sur l'url http://localhost:8080/discotheque/. Cliquer sur "Vérification des données: CheckData". Le code de contrôle est le même que l'exemple 4.4.1. Vérification du nombre d'instance.

Si tout se passe correctement l'application vous répond:

 
Sélectionnez

	3 albums de Pink Floyd, 1 de Television et le best of d'Enio Morricone
	
	Nombre d'instance de class org.moussaud.data.Album = 5
	
	Pink Floy et Television
	
	Nombre d'instance de class org.moussaud.data.Group = 2
	
	Enion Moricone, les 5 membres de PF
	
	Nombre d'instance de class org.moussaud.data.Person = 6
	
	Groupe + Person
	
	Nombre d'instance de class org.moussaud.data.Artist = 8
	
	Tracks
	
	Nombre d'instance de class org.moussaud.data.Track = 38
	
	Genre: rock, blues
	
	Nombre d'instance de class org.moussaud.data.Genre = 2
				

3.5. Requêtes

Nous allons maintenant demander à l'application Discotheque de nous ramener l'ensemble des albums avec leur genre; une fois non trié, une fois trié. Lancer votre navigateur sur l'url http://localhost:8080/discotheque/. Cliquez sur "Requete sur les données: SortData". Ce service va effectuer une requête JDO sans rien préciser. Ensuite il réitère la même requête en demandant un tri au niveau du nom. Le code de contrôle est le même que l'exemple 4.4.2. Les Tris.

Si tout se passe correctement l'application vous répond:

 
Sélectionnez

Liste des Albums (non triés)

    * The Wall - rock
    * Dark Side Of The Moon - rock
    * Wish you Were Here - rock
    * Marquee Moon - rock
    * Compilation - Blues

Liste des Albums (triés)

    * Compilation - Blues
    * Dark Side Of The Moon - rock
    * Marquee Moon - rock
    * The Wall - rock
    * Wish you Were Here - rock
				

Voici donc une application utilisant JDO (Driver JPox) avec le web container J2EE Tomcat. La section suivante va détailler quelques points particuliers.

4. Creusons !

Dans cette partie nous allons décrire l'exemple - discotheque.war - et s'attarder sur deux points en particulier:

  • l'accès au persistence manager et
  • la nouvelle fonctionnalité de JDO 2.0, détachement et attachement des objets.

4.1. Conception de "Discothèque"

L'application Discotheque est articulée pour les 3 services sur un modèle classique:

Image non disponible
Conception

Le navigateur envoie la requête sur une Servlet qui s'appuie sur une couche service pour répondre à sa requête (Classe DiscothequeManager). Cette couche utilise les API JDO pour créer et manipuler le modèle et répondre au service. Les données récupérées, la couche présentation (ui) appelle une JSP pour l'affichage.

Le motif de conception est le MVC, Modèle Vue Contrôleur, sans Struts !

4.2. L'accès au persistence manager

La classe DiscothequeManager est un mélange entre une classe service et une classe DAO. En effet, dans notre exemple simple, il n'est pas nécessaire de séparer les 2 couches. Dès qu'une méthode a besoin d'accéder à JDO, elle a besoin d'une instance de la classe PersistenceManager. Cette classe va lui offrir de pouvoir faire persister des objets, les détruire, faire des requêtes. L'application s'exécute dans un environnement serveur, il n'est pas concevable, à chaque requête:

  1. de charger les propriétés JDO / JPox
  2. de créer une instance de la classe PersistenceManagerFactory
  3. d'ouvrir une instance de la classe PersistanceManager

En particulier l'étape #2 est très consommatrice de ressources: lecture de la configuration JDO, initialisation des paramètres internes, connexion à la base de données,....

Donc la classe DiscothequeManager utilise une classe PMHelper qui va lui fournir une instance de PersistenceManager qui correspond au contexte d'exécution (Méthode a()).Cette méthode associe une instance de PersistenceManager au Thread courant via l'objet magique ThreadLocal. Si le thread courant n'est pas associé avec une instance, il en instancie une nouvelle et lui associe. Sinon, le thread est déja associé à un PersistenceManager, il lui retourne celle existant. Ce pattern permet à toute méthode d'obtenir une instance de PersistenceManager sans nécessiter de la passer de méthode en méthode. L'objet ThreadLocal nous assure qu'il n'y aura de collisions en cas d'exécutions parallèlse: chaque requête est associée à un thread géré par le serveur d'application. Par contre, il est absolument nécessaire de faire un fermer l'association (PMHelper.closePM()) en fin de service.

PMHelper
Sélectionnez

package org.moussaud.service;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class PMHelper {

	private static final ThreadLocal pms = new ThreadLocal();

	private static PersistenceManagerFactory pmf = null;

	public static PersistenceManager currentPM() {

		PersistenceManager pm = (PersistenceManager) pms.get();
		if (pm == null) {
			pm = getPMF().getPersistenceManager();
			pms.set(pm);
		}
		return pm;
	}

	public static void closePM() {
		PersistenceManager pm = (PersistenceManager) pms.get();
		pms.set(null);
		if (pm != null)
			pm.close();
	}

	private static PersistenceManagerFactory getPMF() {
		if (pmf == null) {
			pmf = JDOHelper.getPersistenceManagerFactory(Utils
					.getJDOProperties());
		}
		return pmf;
	}
}

Ce pattern est couramment utilisé dans les frameworks, et en particulier les frameworks de persistance. Exemple: avec Hibernate 2.x, Thread Local Session

4.3. Détachement des objets

L'un des problèmes que l'on rencontre quand on interagit avec un framework de persistance ou un ResultSet JDBC, c'est que les objets retournés sont en lien plus ou moins direct avec la source de données. Ceci est très fâcheux quand on doit les transmettre à une page JSP ou à des méthodes métiers en dehors de la transaction courante.

Généralement la solution envisagée est de créer une famille de Java Bean, ressemblant généralement au modèle, pour accueillir les données. Ce pattern est connu sous le nom de DTO (Data Transfert Object) ou VO (Value Object). Avec schéma, les applications se retrouvent avec une quantité importante d'objets et de services qui font le transfert entre le modèle DTO et le modèle persisté. La valeur ajoutée n'est pas énorme mais le temps de développement, de maintenance et d'exécution augmente !

Les frameworks de persistance tels que JDO ou Hibernate ont bien compris ce besoin (ou cette nécessité) de pouvoir manipuler ces objets de manière déconnectée de la base de données. En JDO, ces méthodes existent: PersistenceManager.attach() et PersistenceManager.detach().

Dans notre exemple, nous allons voir comment utiliser détacher des instances pour transmettre la liste des albums et des genres directement à la JSP pour affichage.

4.3.1. La requête

Si on regarde la requête faite pour trier les albums on a le code suivant:

 
Sélectionnez

public Collection sortData(boolean sorted) {
	try {

		PersistenceManagerFactory pmf = JDOHelper
				.getPersistenceManagerFactory(Utils.getJDOProperties());
		PersistenceManager pm = pmf.getPersistenceManager();
		Transaction tx = pm.currentTransaction();
		pm.getFetchPlan().addGroup("detach_album_genre");
		tx.begin();
		Query qt = pm.newQuery(Album.class);
		if (sorted)
			qt.setOrdering("name ascending");
		Collection c = (Collection) qt.execute();
		c = pm.detachCopyAll(c);
		tx.commit();

		return c;
	} finally {
		PMHelper.closePM();
	}
}

Si on compare au code utilisé dans la version autonome du sample, On remarque deux lignes supplémentaires

  • pm.getFetchPlan().addGroup("detach_album_genre"). Cette ligne indique au persistence manager que l'on va ajouter au plan de récupération par défaut d'autres instances. Par défaut, quand un objet est récupéré par le persistence manager, le driver s'arrête à ce que j'appele les types simples ou basiques. Toutes relations de type 1..1 ou 1..n ne sont pas du tout récupérées. Elles le seront à la demande. Dans notre exemple nous allons augmenter la récupération par défaut en y incluant une relation 1..1 vers la classe Genre
  • c = pm.detachCopyAll(c) Cette methode permet de détacher les instances d'une collection du PersistenceManager avec laquelle elles sont liées. (en concordance avec le plan de récupération - le fetch plan).

4.3.2. Définition du plan de récupération (Fetch Plan)

Si on regarde de près le fichier package.jdo, on voit pour la classe Ablum, la définition suivante:

 
Sélectionnez

<class name="Album" detachable="true" identity-type="datastore">
  <field name="artist"/>
  <field name="date"/>
  <field name="genre"/>
  <field name="name"/>
  <!-- relation 1..n implementee sous forme de List (arraylist)
  contenant des instances de la classe Track -->
  <field name="tracks">
    <collection element-type="org.moussaud.data.Track"/>
    <join/>
  </field>
  <!-- Definition d'un fetch groupe qui contient les pistes -->
  <fetch-group name="detach_album_genre">
    <field name="genre"/>
  </fetch-group>
  <!-- Definition d'un fetch groupe qui contient l'artiste et le genre -->
  <fetch-group name="all_details">
    <field name="artist"/>
    <field name="genre"/>
    <field name="tracks"/>
  </fetch-group>
</class>

En plus de la désormais classique définition des champs (field) on voit maintenant apparaître la définition des fetch groups. Ces fetch groups définissent explicitement quelles instances de classes doivent être récupérées (chargées) en même temps que l'instance. Cette récupération étendue peut être nécessaire comme notre exemple pour transmettre des objets mais aussi pour indiquer au driver JDO que l'on avoir besoins de ces instances et qu'il doit les récupérer en une seule fois (optimisation possible au niveau de l'implémentation). Exemple: si pour un traitement, on a besoin de parcourir l'ensemble des pistes d'un album pour calculer la durée total de l'Album, autant définir un fech group sur les instances de type Track.

Il est possible de définir autant de fetch groups que l'on souhaite pour une classe donnée. On peut voir ces fetch groups comme des Cas d'Utilisation. Exemple:

  • Cas d'utilisation 1: Requête pour afficher le nom des albums (fetch group defaut) puis
  • Cas d'utilisation 2: Détail d'un album (fetch group all_details, ajout des attributs artists,genre et tracks)

5. Conclusion

Cet article vous a montré comment l'utilisation d'un composant de persistance tel que JDO pouvait s'intégrer facilement au sein d'un serveur Tomcat, en apportant performance et simplicité dans la persistance vos modèles métiers.

Une étape supplémentaire pour une conception complète serait d'y ajouter l'utilisation du framework Spring. Spring permet de prendre en charge

  • la gestion du persistence manager et de sa factory,
  • la gestion des transactions de façon déclarative (comme dans EJB)
  • de l'assemblage des différentes couches (web, services, métiers, données)

Source complet de l'application est disponible en Téléchargement (WorkSpace Eclipse 3.1 avec WTP)

5.1. Crédits et License

Ce tutorial est écrit par Benoit Moussaud, Consultant Sénior en Architecture J2EE chez Xebiaxebia
Pour un contact ou des précisions, n'hésitez pas à m'écrire:
Copyrights Benoit Moussaud 2005