Sep 11, 2016 1:34:41 PM Pierre LEVY avatar   282    

Utilisation de JPA

Pourquoi utiliser JPA ?

La couche d'accès aux données de Lutece, basée sur du SQL natif et inspirée du modèle EJB Entity 1.x en POJO (Plain Old Java Object), est certes simple et robuste mais repose sur une implémentation spécifique ayant des limitations (code peu synthétique et répétitif, gestion transactionnelle complexe).

JPA est une API normalisée très récente. Elle fait partie de la spécification EJB3 de la plate-forme JEE 5.0 qui a vu le jour en 2007 (JPA 2.0 vient de sortir à l'occasion de JEE 6.0).

Cette API est une couche d'abstraction permettant d'utiliser diverses implémentations (Hibernate de Red Hat, Toplink d'Oracle, OpenJPA, Datanucleus/Google AppEngine, ...) et ne requiert plus impérativement un container EJB car basée sur des POJO.

Au niveau fonctionnel JPA supporte des mécanismes d'héritage, offre des facilités d'écriture de service transactionnels et un langage de requête évolué.

Les principaux concepts

Les objets Entity : Ce sont les objets métiers. Ils constituent ce que l'on appelle généralement le "model".

L' Entity manager : C'est le gestionnaire d'entités qui assure le chargement et l'écriture des objets à partir de la base de données et la gestion de ces objets en mémoire. Cet objet est généralement instancié par une factory fournie par l'implémentation ORM que l'on aura retenu.

Les DAO : Ces sont les objets chargés d'effectuer les opérations d'accès aux données (Data Access Object). Dans JPA la notion de DAO a sensiblement évolué. En effet, alors qu'à l'origine ces objets exécutaient le code de bas niveau pour manipuler la base de données, ces opérations sont aujourd'hui assurées par le framework de persistance. Leur rôle dans JPA se rapproche désormais plus du rôle de l'objet Home.

Les persistence units : Ces sont les unités de gestion de la persistance. Elles définissent notamment les informations d'accès base de données, ainsi que toutes les options applicables à cette unité (mode transactionnel, isolation, ...).

Les principes et particularités de la nouvelle implémentation de JPA dans Lutece

La transformation des classes métiers en entités

Cette transformation comprend plusieurs aspects

  • Les identifiants des classes doivent être des classes et non des types de base. Les int doivent être convertis en Integer ou Long.
  • Les entités doivent être Serializable
  • Des annotations sont à ajouter :
    • au niveau de la classe
      • @Entity : Pour indiquer que cette classe correspond à une entité
      • @Table : Pour lier cette entity à une table de la base de données
    • au niveau des getters
      • @Id : Pour indiquer que ce getter correspond à la clé primaire de l'entité
      • @GeneratedValue : Pour compléter @Id si l'on veut que la clé primaire soit générée
      • @Column : Pour lier le champ à une colonne de la table

Voici le code d'un objet métier avant transformation

/**
 * This class represents business objects Level
 */

public class Level
{
    private int _nId;
    private String _strName;

    /**
     * Returns the level right identifier
     *
     * @return the level right identifier
     */
    public int getId(  )
    {
        return _nId;
    }

    /**
     * Sets the level right identifier
     *
     * @param nId the level right identifier
     */
    public void setId( int nId )
    {
        _nId = nId;
    }

    /**
     * Returns the name of the level right
     *
     * @return the level right name
     */
    public String getName(  )
    {
        return _strName;
    }

    /**
     * Sets the name of the level right
     *
     * @param strName the level right name
     */
    public void setName( String strName )
    {
        _strName = strName;
    }
}

Voici le code transformé pour JPA

/**
 * This class reprsents business objects Mode
 */
@Entity
@Table(name = "core_level_right")
public class Level implements Serializable
{
    private Integer _nId;
    private String _strName;

    /**
     * Returns the level right identifier
     *
     * @return the level right identifier
     */
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name = "id_level", unique = true, nullable = false)
    public Integer getId(  )
    {
        return _nId;
    }

    /**
     * Sets the level right identifier
     *
     * @param nId the level right identifier
     */
    public void setId( Integer nId )
    {
        _nId = nId;
    }

    /**
     * Returns the name of the level right
     *
     * @return the level right name
     */
    @Column(name = "name")
    public String getName(  )
    {
        return _strName;
    }

    /**
     * Sets the name of the level right
     *
     * @param strName the level right name
     */
    public void setName( String strName )
    {
        _strName = strName;
    }
}

L'EntityManagerService

Il est important de conserver la possibilité pour chaque plugin d'accéder à une base de données spécifique qui n'est pas celle du portail. Il s'agit donc de conserver la notion de pool de connexions associé au plugin. Il est ainsi nécessaire de pouvoir disposer d'autant d'objets EntityManager qu'il y a de bases de données spécifiques à gérer pour les plugins d'une instance Lutece. L'EntityManagerService a donc pour responsabilité de fournir à chaque plugin l'EntityManager correspondant à la base de données à laquelle il est affecté. Le principe d'affectation dans la gestion des plugins du back office d'un pool (ou plutôt d'une persistence unit désormais) reste conservé. Il s'agit, pour résumer, d'une Map contenant les EntityManagerFactory initialisées par le JPAStartupService. La création de l'EntityManager est déléguée à Spring qui gère les transactions. Le DAO Générique

Les generics introduits par Java 5 et le mode de gestion de la persistance proposé par JPA permettent de réaliser des composants génériques.

Voici l'interface du fr.paris.lutece.util.jpa.IGenericDAO proposé par Lutece

/**
 * Class GenericDAO
 * @param <K> Type of the entity's key
 * @param <E> Type of the entity
 */
public interface IGenericDAO<K, E>
{
    /**
     * Create an entity
     * @param entity The entity to create
     */
    void create( E entity );

    /**
     * Update an entity
     * @param entity An entity that contains new values
     */
    void update( E entity );

    /**
     * Remove an entity
     * @param key The key of the entity to remove
     */
    void remove( K key );

    /**
     * Find an entity by its Id
     * @param key The entity key
     * @return The entity object
     */
    E findById( K key );

    /**
     * Find all entities
     * @return A list of entities
     */
    List<E> findAll(  );

    /**
     * Synchronize the persistence context to the underlying database.
     */
    void flush(  );

    /**
     * Remove the given entity from the persistence context, causing a managed entity to become detached.
     * @param entity the entity
     */
    void detach( E entity );
}

Plusieurs classes abstraites prennent en charge l'implémentation générique :

  • JPAGenericDAO : Implémentation générique indépendante du core de Lutèce.
  • JPALuteceDAO: Implémentation intégrant la récupération de l'EntityManager à partir de l'EntityManagerService de Lutece. La méthode donnant le nom du plugin est à surcharger pour sélectionner l'EntityManager. Cette classe se trouve au niveau du package fr.paris.lutece.portal.service.jpa.
  • JPALuteceCoreDAO: Idem que JPALuteceDAO mais aucune méthode à surcharger ; l'EntityManager du core est fournie par défaut.

Aucune de ces classes n'utilise et ne doit utiliser des classes provenant d'implémentations spécifiques telles que Hibernate. Les seuls références externes doivent se baser sur les packages javax.persistence.*.

L'implementation d'un DAO simple du core, tel que LevelDAO, se réalise alors comme suit :

/**
 * Class LevelDAO
 */
public class LevelDAO extends JPALuteceCoreDAO< Integer , Level> implements ILevelJpaDAO
{
}

ou autre exemple classique incluant une méthode supplémentaire pour fournir une ReferenceList

/**
 * Class RoleJpaDAO
 */
public class RoleJpaDAO extends JPALuteceCoreDAO< String , Role> implements IRoleJpaDAO
{

    public ReferenceList selectRolesList()
    {
        ReferenceList list = new ReferenceList();
        for( Role role : findAll() )
        {
            list.addItem(role.getRole(), role.getRoleDescription());
        }
        return list;
    }
}

Pour un DAO d'un plugin pouvant accéder à une base de données spécifique, voici le code d'exemple

/**
 * Class PluginDAO
 */
public class PluginDAO extends JPALuteceDAO< String , Role> implements IRoleJpaDAO
{
    private static final String PLUGIN_NAME = "myplugin";

    @Override
    public getPluginName()
    {
        return PLUGIN_NAME;
    }
}

Ce DAO ne dérive plus de la classe JPALuteceCoreDAO, mais de JPALuteceDAO. Il doit donc implémenter la méthode getPluginName(). L'implémentation à retenir

Implémentations JPA

JPA étant une API, plusieurs implémentations peuvent être utilisées pour assurer la persistance. Toutes n'offent néanmoins pas les mêmes fonctionnalités ni la même couverture de la norme ; par exemple l'implémentation JPA de datanucleus est loin de couvrir l'ensemble de la norme.

Une seule implémentation est actuellement intégrée à Lutèce : Hibernate (il est possible que la version ci-dessous ne soit pas la dernière mise à jour)

<dependency>
     <groupId>fr.paris.lutece.plugins</groupId>
     <artifactId>module-jpa-hibernate</artifactId>
     <version>1.0.7</version>
     <type>lutece-plugin</type>
</dependency>

Il est toutefois possible d'intégrer une autre implémentation en se basant sur le jpa-hibernate_context.xml, l'intégration reposant principalement sur Spring + JPA. Voir le fonctionnement de JPAStartupService pour plus d'information sur les données récupérées dans le XML et l'intégration avec le(s) transactionmanager(s) et les propriétés récupérées dans le context.

Le fichier de configuration principal (utilisable par défaut, la configuration se fait généralement pour les développements ou les optimisations de cache). A noter que la configuration du datasource n'est pas à faire, et utilise les propriétés du db.properties.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.0.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">


       <bean id="jpaStartupService" class="fr.paris.lutece.portal.service.jpa.JPAStartupService" />

       <bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />

       <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
       			<!-- uncomment to view SQL -->
                <!-- cette valeur peut être positionnée à true lors des développements -->
                <!-- <property name="showSql" value="true" />  -->
       </bean>


        <bean id="jpaPropertiesMap" class="java.util.HashMap" >
        <constructor-arg>
         <map>
                <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <entry key="hibernate.format_sql" value="true" />
                <entry key="hibernate.id.new_generator_mappings" value="true" />

                <!-- db generation -->
                <!-- uncomment the following line to enable db generation -->
                <!-- cette valeur peut être positionnée à update lors des développements -->
                <!-- <entry key="hibernate.hbm2ddl.auto" value="update" />  -->

                <!-- Cache configuration -->
                <!-- uncomment the following lines to enable caching -->
                <!--
                <entry key="javax.persistence.sharedCache.mode" value="ENABLE_SELECTIVE" /> 
                <entry key="hibernate.cache.use_query_cache" value="true" />
                <entry key="hibernate.cache.use_second_level_cache" value="true"/>
                <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.SingletonEhCacheRegionFactory"/> 
                -->
         </map>
        </constructor-arg>
       </bean>

       <!-- Monitoring (utilisation de cache...) -->
       <bean id="jpa-hibernate.monitorService" class="fr.paris.lutece.plugins.jpa.modules.hibernate.service.HibernateMonitorService">
       	<property name="entityManagerService" ref="entityManagerService" />
       	<property name="JMXEnabled" value="false" />
       	<property name="statisticsEnabled" value="false" />
       </bean>
</beans>

La map jpaPropertiesMap contient les propriétés d'initialisation d'Hibernate. Voir [1] pour plus d'informations sur les propriétés disponibles (notamment hibernate.dialect pour les cas où la base n'est pas MySQL5).

Les statistiques sont disponibles en Back Office seulement lorsqu'elles sont activées (désactivées par défaut).

Gestion MultiPools

Le multipool est géré de façon "simpliste" : création et commit/revert des transactions sur l'ensemble des datasources. Il est possible d'intégrer des mécanismes de commit plus robustes avec JTA (Atomikos par exemple) avec la configuration Spring.

Dans le cas d'un multipool avec des bases de type différent (exemple MySQL et PostgreSQL), il est possible de surcharger le dialect pour chacun des pool dans le db.properties : exemple avec le pool portal en mysql et le pool monPlugin en PostgreSQL :

...
portal.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
...
monPlugin.dialect=org.hibernate.dialect.PostgreSQLDialect

Par défaut, org.hibernate.dialect.MySQL5InnoDBDialect est utilisé - il s'agit en réalité la propriété hibernate.dialect du fichier jpa-hibernate_context.xml.