User Tools

Site Tools


wiki:epims3_3:developer:ep-core

Guide d'implémentation d'eP-Core

Introduction

eP-Core est le module d'accès à la BD. Ce module permet d'obtenir les objets Java représentant les données de la BD ePims ainsi que les services permettant d'agir sur ces objets. Ce module est déployé de façon indépendante sur le serveur d'application Java Geronimo afin d'être utilisé et partagé par les autres modules de présentations et de service du système.

Technologies

Figure 1 Architecture globale de l’application

Le schema représente la totalité de l'application ePims. eP-Core représentele noyau de cette application, il ne contient pas ce qui se rapporte à la couche présentation càd interfaces Web, il constitue le point d’accès aux données de l’application ainsi qu’aux actions sur ces données (création, modification, suppression) au travers d’interfaces accessibles par toutes les couches de l’application.

Comme indiqué sur ce schéma, les technologies utilisées dans l'application ePims sont les suivantes:

  • Les JSP/JSF sont utilisés pour la partie présentation
  • Au niveau de la logique applicative, on utilise le framework Spring qui est un conteneur léger et qui permet, d’une part, d’isoler les différentes technologies utilisées et faciliter ainsi la substitution d’une implémentation par une autre, d’autre part, Spring prend en charge la partie transactionnelle de O/RM.
  • Hibernate a été choisi pour la partie O/R Mapping. Ce framework a, entre autres, l’avantage de s’interconnecter facilement avec Spring.

Structure du projet

Les cibles Ant

Les cibles Ant principales définies pour eP-Core sont :

  • configure.dist : Configure le projet pour une distribution de production (copie des fichiers de configutations, mise en place de librairies…)
  • compile.dist : compile les classes Java et appel configure.dist.
  • configure.test : Même configuration que précédement mais utilise les fichiers de configuration pour exécuter les tests JUnit.(appel de configure.dist)
  • compile.test : compile les classes Java et appel configure.test.
  • configure.dev : Même configuration que configure.dist mais utilise les fichiers de configuration pour le mode développement.(appel de configure.dist)
  • compile.dev : compile les classes Java et appel configure.dev.
  • dist : créer une distribution de ep-Core comme module geronimo … appel toutes les cibles nécessaire à cette création
  • dist.dev : idem dist mais appel compile.dev au lieu de compile.dist.
  • dev.create.onejar : Temporaire ! Créer un seul .jar contenant toutes les librairies (lib/*) ainsi que eP-Core.jar créer lors d'une execution de dist. N'appèle pas dist qui doit donc avoir été exécuté avant !
  • publish.libs : a exécuter après un dist. N'appèle pas dist qui doit donc avoir été exécuté avant ! Copie dans un sous-répertoire défini par la propriété <ivy.distrib.dir> toutes les librairies nécessaire a eP-Core et eP-Core. Ceci afin de permettre a eP-Web, par exemple, de référencer ces librairies sans les intégrer. En effet; dans la distrib finale d'eP-Web (ou autre module de l'application J2EE) ne doit pas intégrer eP-Core mais référencera cette librairie intégrée a Geronimo.

Architecture

Cette section décrit l’organisation des packages ainsi que les points d’interaction entre les objets des différents frameworks.eP-Core ne concernant pas tout ce qui se rapporte à la partie présentation, seuls les objets de persistance de données et ceux propre à la logique applicative sont définis.

La théorie

Figure 2 Diagramme de classe globale

  • Les objets du package BO (BusinessObject) représentent les données du model (en l’occurrence celles de la base de données). Ces objets ne contiennent que des informations (des attributs) mais pas de ‘comportements’. Ils sont accessibles dans toutes les couches de l’application.
  • Le package DAO contient les classes qui ont la charge de la persistance des données (utilisation du framework Hibernate). Seules les interfaces doivent être accédées afin de permettre tout changement d’implémentation de manière transparente. En effet, le framework Spring permet de spécifier (aux classes services) la classe d’implémentation via le mécanisme d’inversion de contrôle (IoC) par injection par accesseur : utilisation d’un fichier de configuration et accès aux setter méthodes. Spring prend également en charge la gestion des sessions/transactions pour Hibernate : les classes DAO doivent étendre la classe HibernateDaoSupport qui prend en charge les accès JDBC.
  • Le package Service contient les interfaces qui doivent être utilisées par toutes les autres applications (ou couches) afin de manipuler (créer, modifier, supprimer) les objets de données. Les classes d’implémentation des services doivent définir des setter méthodes pour les classes DAO afin d’être configurées par Spring (voir paragraphe précédent).

En résumé, la couche model rend visible d’une part les objets représentant les données (package BO) et, d’autre part, les interfaces Services qui permettent de manipuler ces données.

On retrouve donc dans les fichiers de configuration xml : (spring-*.xml)

  • La définition de la dataSource utilisée par Hibernate pour accéder à la DB.
  • La définition des différentes propriétés propres à Hibernate (mapping files…)
  • La définition des dao : description des beans et spécification de la propriété hibernateTemplate.
  • La définition des services : description du bean et spécification des propriétés qui seront positionnées via les accesseurs.

REMARQUE : ServiceLocator recherche dans l’ApplicationContext qui lui est spécifié les beans correspondants à l’implémentation de IXXXService (définit dans spring-*.xml).

La pratique

Utilisation d'un générateur d'Objets

La génération des POJOs et DAO se fait via MyEclipse Hibernate Reverse Engineering (HRE). Les hbms sont construits manuellement. Afin de permettre une séparation des POJOs et des DAO dans des packages différents la génération se fait en deux étapes :

Génération des DAO

Depuis MyEclipse Database Explorer, sélectionner toutes les tables de la BD ePims sauf :

  1. prog_contact
  2. proj_contact
  3. study_contact
  4. samples_share

Dans les fenêtres d'HRE

  • sélectionner le répertoire source “src/main” et le domain “cea.edyp.epims.domain.dao.impl”. Sélectionner seulement la génération des DAO (en vérifiant que l'option “generate precise findBy methods” est décochée) et l'utilisation de template maison, en précisant le chemin “{workspace}/ep-Core\resources\templates”, comme ci-dessous :

Figure 3 MyEclipse Hibernate Reverse Engineering

  • utiliser le fichier ./hibernate.reveng.xml comme “rev-eng settings file”. Ce fichier permet de spécifier, entre autre, des nom de classes différentes que ceux proposés par défaut pour certaines tables :
      o table ms_protocol  => classe cea.edyp.epims.domain.dao.impl.MSProtocol
      o table msms_protocol => classe cea.edyp.epims.domain.dao.impl.MSMSProtocol
      o table gel_1d => classe cea.edyp.epims.domain.dao.impl.Gel1D
      o table gel_2d => classe cea.edyp.epims.domain.dao.impl.Gel2D
      o table gel_1d_migration  => classe cea.edyp.epims.domain.dao.impl.Gel1DMigration
      o table gel_2d_migration  => classe cea.edyp.epims.domain.dao.impl.Gel2DMigration
      o table sample_species => classe cea.edyp.epims.domain.dao.impl.Species
      o table sample_subcellular_localisation => classe cea.edyp.epims.domain.dao.impl.SubCellularLocalisation

Une fois les classes DAO générées il faut corriger les erreurs suivantes :

  • ActorRoleDAO, méthode findByID:

public ActorRole findById( cea.edyp.epims.domain.dao.impl.ActorRoleId id) ⇒ public ActorRole findById( cea.edyp.epims.domain.ActorRoleId id)

  • TreatmentsApplicationDAO, méthode findByID: idem ci dessus
  • StudyDAO, l'interface IStudyDAO qu'implémente cette classe définit en plus des méthodes “génériques” la méthode refreshLazy(). Par conséquent, cette classe doit êtredéfinie comme abstraite. La classe EPCoreStudyDAO, sous-classe de StudyDAO, implémente cette méthode.

Attention : A la première création des DAO, nous avons “extrait” les interfaces (via MyEclipse) dans le package cea.edyp.epims.domain.dao.*. Ceci afin de référencer les interfaces dans les services et éventuellement d'enrichir ces interfaces pour certaines classes. Dans ce cas des modifications sur les classes DAO générées sont nécessaires. Par exemple pour l'objet Study :

  • Les objets du domaines cea.edyp.epims.domain.AbstractStudy et cea.edyp.epims.domain.Study sont généres (cf ci-dessous)
  • La classe cea.edyp.epims.domain.dao.impl.StudyDAO a été générée et l'interface cea.edyp.epims.domain.dao.IStudyDAO a été extraite de cette classe.
  • La méthode refreshLazy() a été ajoutée à l'interface IStudyDAO. Nous avons défini une classe EPCoreStudyDAO dérivée de StudyDAO qui implémente cette méthode. Par contre, la classe StudyDAO n'implément pas toutes les méthodes de IStudy, par conséquent une erreur est levée. Pour remédier à cela, la classe StudyDAO est déclarée abstraite.

Figure 4 Diagramme de classes exemple pour les DAO Hibernate

Génération des POJO

Les Objets du domaines sont générés depuis les fichiers *.hbm.xml:

  • Sélectionner tous les fichiers hbm.xml,
  • Cliquer droit avec la souris et choisir “MyEclipse → Generate POJOS”
  • Dans la fenêtre “Hibernate Foward Engineering”
     o spécifier src/main comme output folder, 
     o cocher la case "Create abstract class"
     o ne pas cocher la case custom template

Seules les classes mères “AbstractXXX” sont systématiquement générées. Les sous classes ne sont générées que la première fois ce qui permet de les personnaliser sans tout perdre à chaque génération.

Figure 5 diagramme de classes exemple pour les objets du domaine

Définition des Services

Accès:

Seules les interfaces IxxxService sont visibles depuis toute autre application (telle que la couche présentation) qui utilisera eP-Core. Ces interfaces devront fournir les méthodes nécessaires pour l’accès aux données en lecture et en écriture.

Règles de développement

Les méthodes pour l'écriture des données sont de la forme :

  • public Sample saveSample(Sample sample)throws EPimsServiceException; Sauvegarde d'un élément (ici Sample) déjà existant. L'objet saivegardé est retourné.
  • public Sample createSample(Sample sample) throws EPimsServiceException; Création d'un nouvel élément. L'objet créé est retourné.
  • public void deleteSample(Sample sample) throws EPimsServiceException; Suppression d'un élément

Pour les accès en lecture, les services sont écrits selon le principe suivant :

  • on définit un objet enum contenant un ensemble de requêtes retournant des objets (du type de l'entité que représente le service) en appliquant des filtres specifics.
  • on définit une méthode générique (getSamples(enum.query, args)) permettant d'exécuter ces requêtes. Cette méthode reçoit une des requêtes prédéfinies en paramètre ainsi que le/les argument(s) dont peut avoir besoin cette requête, et retourne une liste des valeurs resultantes.

Il est certain que certaines actions ne peuvent être appliquées par une simple requête (aussi compliquée qu'elle puisse être !!), à ce moment là, les services peuvent aussi avoir des méthodes exécutant des tâches partiulières.

Dans Spring:

Et au niveau des fichiers de configuration, on définit les beans.

  • Dans le fichier SpringAppContext.xml :
      <bean id="contactServiceTarget" class="cea.edyp.epims.service.impl.HibernateContactService"> 
         <property name="actorDao"><ref bean="ActorDAO"></ref></property>
         <property name="contactDao"><ref bean="ContactDAO"></ref></property>
         <property name="actorRoleDao"><ref bean="ActorRoleDAO"></ref></property>
         <property name="companyDao"><ref bean="CompanyDAO"></ref></property>
      </bean>
        <!-- Transactional proxy Services -->   
      <bean id="baseTransactionProwy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"  abstract="true">
         <property name="transactionManager"><ref local="transactionManager"/></property>
         <property name="transactionAttributes">
             <props>
                 <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                 <prop key="save*">PROPAGATION_REQUIRED</prop>
                 <prop key="create*">PROPAGATION_REQUIRED</prop>
                 <prop key="delete*">PROPAGATION_REQUIRED</prop>
             </props>
         </property>
     </bean>
     ...
     <bean id="contactService" parent="baseTransactionProxy">
        <property name="target"><ref local="contactServiceTarget"/></property>
    </bean>
  • Et pour les DAOs :
    <bean id="actorDAO" class="cea.edyp.epims.domain.dao.impl.HibernateActorDAO"  >
        <property name="hibernateTemplate"><ref bean="hibernateTemplate"/></property>
    </bean>

Définition des Packages

Tous les packages du projets sont des sous packages de cea.edyp.epims. Les classes relatives aux données sont sous les packages cea.edyp.epims.domain. Chacun de ces packages contient un package base où Hibernate génère les classes relatives aux données et un package dao où Hibernate génère les interfaces relatives à l’accès des données dans la base, les classes d’implémentation de ces interfaces sont générées dans le sous package dao.impl, package relatif aux packages des domaines.

Les interfaces fournissant les services (voir section précédente) sont regroupées dans le package cea.edyp.epims.service. Ces interfaces seront implémentées par les classes se trouvant dans le package cea.edyp.epims.service.impl

Utilisation de Spring

Configuration

Les différentes configurations nécessaires à Spring sont faites dans des fichiers XML : springContext.xml, springAppContext.xml et springDataSource.xml.

  • springContext.xml : C'est le point d'entrée, ce fichier créée un ApplicationContext, ayant epCore.context comme id, dans lequel est chargé tout l'environnement contenu dans les deux autres fichiers.
  • springAppContext.xml : Contient la définition des beans de eP-Core, à savoir les bean DAO utilisé par les beans Services également définis dans ce fichier. C'est aussi dans ce fichier que l'on trouve la configuration relative à hibernate. On retrouve dans ce fichier les définitions

o Des propriétés de hibernate via le bean SessionFactory,

        o Du bean HibernateTemplate utilisé par les DAO
        o Les différents beans DAO
        o Les différents bean de services ainsi que les beans TransactionManager associés.
  • springDataSource.xml : Définition de la datasource permettant d'accéder à la BD. Ce module étant déployé sur Geronimo, on peut référencé la datasource déclarée au niveau de ce serveur (DS JNDI).

Les modules ePims déclaré dans l'application EAR partageront le context spring définit dans eP-Core et donc les beans 'singleton' définis dans ce contexte. Voir la doc de l'architecture ePims …

Pour un module d'application web, il est nécessaire de définir le context spring parent que l'on souhaite utiliser dans le fichier web.xml:

    <!-- Spring Context -->
    <context-param>
      <param-name>locatorFactorySelector</param-name>
      <param-value>classpath*:springContext.xml</param-value>
    </context-param>
    <context-param>
      <param-name>parentContextKey</param-name>
      <param-value>span>epCore.context</param-value>
    </context-param>
    <listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

il est également possible de l’utiliser dans une application :

   String[] confFiles = {"/springAppContext.xml","/springDataSource.xml"};
   ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(confFiles);

Code

Dans le code des applications utilisant le module eP-Core, lorsque l'on souhaite accéder à un des beans(services) définis dans spring, on utilise la classe ServiceLocator :

    ServiceLocator locator = new ServiceLocator(context);
    IStudyService service= locator.getStudyService();

Hibernate

Attention : Si dans les fichiers de mapping (.hbm) on utilise un set pour les collections non ordonnées, on ne peut pas utiliser ces collections dans des 'datatable' (JSF) pour la partie view. ⇒ Utilisation de 'bag' dont l'implémentation en Java est une ArrayList.

Utilisation de la propriété lazy

Les liens entre les objets de la base sont nombreux ce qui fait que lorsque l’on accède à un objet, comme un Program ou un Projet, on finit par rechercher et récupérer dans la base tous les échantillons rattachés à ce programme ou projet ainsi que les préparations et séparations subis par les échantillons etc. etc.…

Afin d’optimiser ces accès aux données on utilise le paramètre lazy de hibernate qui permet pour un attribut donné de ne récupérer les valeurs que ci celles-ci sont explicitement demandées… Par exemple, dans la définition d’une étude on a bien la collection des échantillons associés, mais la collection n’est renseignée que si l’on accède à celle-ci, en demandant la liste des échantillons ou en recherchant un échantillon particulier.

Mapping Hibernate :

   <hibernate-mapping>
     <class
            name="cea.edyp.epims.domain.Study"
            table="study"
     >
      <bag
            cascade="delete"
            inverse="true"
            name="samples"              
            lazy="true"
      >
            <key column="study" />
            <one-to-many class="cea.edyp.epims.domain.Sample" />
      </bag>
    </class>

Note De plus, on peut imaginer ajouter dans les services des méthodes « light » permettant de récupérer, par exemple, la liste des noms des échantillons sans pour autant tout récupérer, en utilisant par exemple un accès directe via JDBC.

Problème :

Cette solution semble parfaite mais le problème réside dans son utilisation. En effet, si l’on tente d’accéder à la propriété dite lazy une fois la session hibernate fermée, une exception est générée. Pour éviter toute exception, il ne faut donc pas utiliser directement les méthodes définies dans les objets du domaine.

   public class StudyBean{
      private  Study study_ ;public List getSamples(){
       if(study_ == null)
         return new ArrayList();
       return study_.getSamples();
     }

Une des solutions que l’on retrouve dans les différents forum ou docs, est de récupérer les données lors de la lecture de l’entité si l’on sait que l’on en aura besoin …. Cette solution n’est pas convaincante lorsque l’on souhaite séparer complètement la vue des données. En effet, lors du chargement d’une entités au niveau des DAO ou même des Services, on ne connaît pas l’utilisation qui en sera faite.

Solution : Il est donc nécessaire de définir une méthode qui ouvre la session si nécessaire afin de charger les données lazy sur demande. Une première méthode est définie au niveau des DAO. On retrouve dans IStudyDAO la déclaration de la méthode public void refreshLazy(Study study). Au niveau de l’implémentation, dans EPCoreStudyDAO, lors d’une même session on actualise l’état de l’entité et on accède aux propriétés lazy afin que celles-ci soient chargées.

On ajoute une méthode au niveau des services (interface et implémentation) pour permettre l’accès aux données :

   public List getStudySamples (Study study) {     
     studyDao_.refreshLazy(study);
     return new ArrayList(study.getSamples());
   }

L'utilisateur d'ePCore verra deux méthodes pour accéder aux données d’une entité : Study.getSample() et IStudyService.getStudySample(Study study). L’utilisation de l’une ou l’autre de ces méthodes entraîne un comportement différent, voire la génération d’une exception dans le premier cas. Un commentaire devra expliquer le comportement de ces méthodes.

Archivage

Services

On fait apparaître au niveau d'eP-Core la notion d’archivage. Il existe un repository, PIMS_ARCHIVE, dédié à l’archivage, un champ archived, est ajouté dans la table acquisitions_result et les études ont un état qui peut être ‘archivable’ ou ‘archived’. Enfin des services permettent d’archiver certaines données.

La classe IStudyService définit la méthode archiveStudy(Study s) qui

  o recrée l’arborescence depuis PIMS_ROOT jusqu’au répertoire de l’étude sous PIMS_ARCHIVE
  o déplace récursivement depuis le répertoire de l’étude tous les fichiers dans le répertoire correspondant sous PIMS_ARCHIVE
  o copie un fichier texte standard, en mettant à jour la date, dans le répertoire (vide) de l’étude
  o change l’état de l’étude dans la base de données à ‘archived’
  o positionne le champs ‘archived’, dans la base de données, des acquisitions associées à l’étude

La classe IAcquisitionService définit la méthode archiveAcquisitionResult(List) qui pour chaque AcquisitionResult :

  o recrée l’arborescence depuis PIMS_ROOT jusqu’au répertoire de l’acquisition sous PIMS_ARCHIVE
  o déplace l’acquisition dans le répertoire créé sous PIMS_ARCHIVE
  o positionne le champ archived, dans la base de données.

L’archivage peut être long puisqu’il consiste en la copie de données pouvant représenter plusieurs GO. C’est pourquoi, l'archivage est fait par un thread indépendant.

ArchiveManager

Quartz

Quartz est une librairie qui permet de gérer des « jobs » et de les programmer afin qu’ils soient exécuter à un moment précis, de manière récursive ou non. L’opération que l’on souhaite exécuter est défini dans une classe implémentant l’interface Job. On définit par ailleurs un Trigger qui s’activera à une certaine date/heure (et qui pourra ensuite se déclencher régulièrement), enfin, on spécifie à la classe Scheduler l’association Job-Trigger ou plus exactement JobDetail-Trigger. La classe JobDetail contenant non seulement la classe Job a exécuter mais également, un identifiant, un groupe d’appartenance et un espace permettant de partager des données avec les instances de Job. Lorsque le Trigger se déclenche, le Job qui lui est associé est exécuté.

Les Job sont exécutés dans des threads, une instance de Job est créée à chaque exécution, ils peuvent lever des exceptions, de type JobExecutionException.

Un autre fichier de propriété quartz.properties est présent sous ../resources/main. Ce fichier est utilisé par Quartz et qui est utilisé pour la gestion des archivages (les opérations sont executées dans des thread).

ArchiveManager

D’une manière plus générale, une classe gère toute la partie archivage, de la définition du répertoire d’archivage à l’archivage proprement dit. Toute les références à l’archivage sont ainsi regroupées en une seule classe. Cette classe, ArchiveManager, utilise Quartz pour l’exécution de l’archivage, opération qui peut être longue et qui doit donc être exécuté dans un thread. L’utilisation de Quartz, permet également aux opérations d’archivage de lever des exceptions en cas d’erreur.

Un autre intérêt de cette définition est qu’il est possible à tout moment de connaître l’ensemble des opération en cours. On pourrait même conserver un historique sur les X derniers archivages.

Figure 7 diagramme de classes : Gestion de l’archivage

Le diagramme de classes ci-dessus représente de façon grossière les classes définit dans le projet Quartz, en jaune, ainsi que les classes spécifique à eP-Core ArchiveManager et ArchiveXXJob. La classe ArchiveManager, qui est un singleton, masque l’utilisation du package Quartz. C’est elle qui crée et démarre le Scheduler. Elle s’enregistre également auprès du Scheduler afin d’être notifiée des évènements relatifs aux Job.

L’utilisateur (développeur) fait appel à l’ArchiveManager lorsqu’il souhaite archiver des acquisitions, dans le cadre d’une étude ou d’un groupe de contrôle ; le Job approprié est alors créé et l’ArchiveManager dédie au Scheduler la gestion de cette opération. L’archivage est exécuté dans un thread à part, créé par le Scheduler. Il est possible d’interroger l’ArchiveManager à tout moment sur l’état de l’archivage : en cours, terminé, en erreur.

Le diagramme de séquence ci-après reprend le scénario d’archivage d’une étude.

Le diagramme de classes ci-dessus représente de façon grossière les classes définit dans le projet Quartz, en jaune, ainsi que les classes spécifique à eP-Core ArchiveManager et ArchiveXXJob. La classe ArchiveManager, qui est un singleton, masque l’utilisation du package Quartz. C’est elle qui crée et démarre le Scheduler. Elle s’enregistre également auprès du Scheduler afin d’être notifiée des évènements relatifs aux Job.

L’utilisateur (développeur) fait appel à l’ArchiveManager lorsqu’il souhaite archiver des acquisitions, dans le cadre d’une étude ou d’un groupe de contrôle ; le Job approprié est alors créé et l’ArchiveManager dédie au Scheduler la gestion de cette opération. L’archivage est exécuté dans un thread à part, créé par le Scheduler. Il est possible d’interroger l’ArchiveManager à tout moment sur l’état de l’archivage : en cours, terminé, en erreur.

Le diagramme de séquence ci-après reprend le scénario d’archivage d’une étude.

Figure 8 diagramme de séquence : Archivage d’une étude

Run Robot

Lors de la validation d'un run robot, plusieurs opérations sont nécessaire. En effet, tant qu'un run n'est pas effectivement validé, on créé des virtual_plate et virtual_well pour organiser les échantillons dans les plaques. Une fois le run robot réalisé, il faut :

  • Créer la plaque
  • Création du process RunRobot
  • Pour chaque échantillon du virtual plate :
    • création du support d'origine si nécessaire
    • création des “wells”contenant l'échantillon sur la plaque
    • créer du transfert du support d'origine vers la plaque
    • création du support final, de même type que celui d'origine
    • créer du transfert de la plaque vers le support final
    • suppression de la demande robot et des virtual_well
    • asscoiation avec le run robot
  • Suppression de la plaque virtuelle.
wiki/epims3_3/developer/ep-core.txt · Last modified: 2008/10/02 10:21 (external edit)