Charger partiellement une entité avec JPA

Il est intéressant, dans nombre de cas, de ne charger que partiellement une entité en JPA. Prennons l'exemple de JTentative, un agrégateur de flux RSS. Il est souhaitable de charger la liste des entrées appartenant à un flux, mais sans forcément charger l'intégralité de chaque entité (contenu et description sont des champs lourds et souvent inutilisés).

Plusieurs solutions existent pour charger partiellement une entité, de ne sélectionner que certains champs à charger depuis la base de données.

Première solution : FetchType.LAZY

La première solution consiste à définir certains champs de notre entité avec un attribut fetch à LAZY. Cela permet de spécifier que ces champs ne sont pas chargés lors du chargement de l'entité, mais uniquement lors du premier accès à ce champ.

L'annotation @Basic nous permet de spécifier ce lazy-loading.

Voici un exemple de code mettant en avant cette annotation. Le code de l'entité à été réduit pour plus de lisibilité :
@Entity
public class Entry implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String title;

    private String author;

    @Lob
    @Basic(fetch=FetchType.LAZY)
    private String description;

    @Lob
    @Basic(fetch=FetchType.LAZY)
    private String content;

    /* Getters, Setters... */
}

Cette solution présente cependant un inconvénient majeur : si elle fonctionne correctement lorsque l'on charge une entité via l'EntityManager, elle est complètement inefficace lors de la sélection de données via une requête JPA-QL.

Seconde solution : Sélectionner uniquement les champs dont on a besoin

Une seconde solution peut-être mise en place dans une requête JPA-QL. Cependant, elle ne fonctionnera paslors de la sélection lors d'un appel à la méthode find() de l'EntityManager.

Cette solution est basée sur un opérateur méconnu du langage JPA-QL, new. L'opérateur new permet d'intancier des objets au sein d'une requête JPA-QL. Il s'agit donc d'instancier un objet, en fournissant au constructeur uniquement les champs que nous souhaitons récupérer.

Voici un autre exemple, le code de l'entité a là aussi été réduit :
@Entity
@NamedQuery(name="Entry.findAll", query="SELECT new Entry(e.id, e.title, e.author) FROM Entry AS e")
public class Entry implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String title;

    private String author;

    @Lob
    @Basic(fetch=FetchType.LAZY)
    private String description;

    @Lob
    @Basic(fetch=FetchType.LAZY)
    private String content;

    public Entry() {
    }

    public Entry(Long id, String title, String author) {
        this.id = id;
        this.title = title;
        this.author = author;
    }

    /* Getters, Setters... */
}

Quelques précisions cependant :
  • Il faut préciser la classe complète de l'entité dans la requête, incluant le nom du package
  • Le constructeur que l'on souhaite invoquer doit exister
  • Prennez garde avec vos entités chargées de cette manière : certaines propriétés sont absentes, une mise à jour dans la base de données avec une entité chargée partiellement risque d'effacer certaines valeurs


Le code complet de l'entité utilisée pour cette exemple est disponible sur le dépôt SVN de JTentative.

Permalink  |  Commentaires (2)

JPA : Utilisation en dehors du module EJB

Récemment, j'ai eu à utiliser JPA pour un site Web. Pour plus de simplicité, je me suis tourné vers un conteneur EJB. Seulement, j'ai eu aussi besoin de créer une multitude de Stateful / Stateless Session Beans, alors que je n'avais vraiment pas besoin de ça. Une application Web toute simple me suffisait largement dans ce cas là.

J'ai alors entrepris de convertir mon application d'entreprise en application Web. Seulement, un problème est apparu assez rapidement : la gestion des transactions de JPA ! En effet, les transactions dont JPA à besoin étaient jusqu'à lors gérées par le conteneur EJB. QUe je n'utilisais plus maintenant.

Je vais donc entreprendre dans cet article un rapide "howto" de l'utilisation de JPA en dehors du module EJB.

1. Mise en place des entités

La première partie est commune à nos deux modes de fonctionnement. Il s'agit de créer nos entités JPA et de créer notre unité de persistance.

On commence avec une entité :
package com.aperigeek.jpa.entity;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    // Getters and setters

}

Puis, on passe à la création de notre unité de persistance (dans notre fichier persistence.xml) :
<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="AperigeekPU" transaction-type="RESOURCE_LOCAL">
    <provider>oracle.toplink.essentials.PersistenceProvider</provider>
    <non-jta-data-source>jdbc/aperigeek</non-jta-data-source>
    <class>com.aperigeek.jpa.entity.User</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
      <property name="toplink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>

La différence par rapport à une unité de persistance gérée par le conteneur EJB est que nous n'allons par utiliser JTA pour la gestion des transactions. Les changements sont :
  • Le transaction-type de notre unité de persistance devient "RESOURCE_LOCAL", pour indiquer que nous allons gérer nous même nos transactions.
  • La DataSource est déclarée grâce aux balises <non-jta-data-source />,. Les DataSources que nous allons utiliser ne seront pas gérées par JTA.


À noter que j'utilise dans cet exemple Toplink (le gestionnaire de persistence par défaut de GlassFish). Les valeurs de la balise <provider> et de la propriété toplink.ddl-generation sont donc spécifique à Toplink.

2. Récupération d'un EntityManager

Étant donné que nous ne sommes plus dans un contexte géré par le conteneur, nous allons devoir récupérer nos EntityManagers à la main. Le code pour récupérer un EntityManager est assez simple. Il suffit de passer par un EntityManagerFactory, qui nous est fourni directement par une méthode statique de la classe Persistence.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("AperigeekPU");
EntityManager em = emf.createEntityManager();

Autre étape très importante : tous les EntityManagerFactory et EntityManager doivent être fermés ! Cette étape de fermeture des ressources est d'habitude laissée au conteneur. Dans notre cas, nous allons devoir le faire nous même :
em.close();
emf.close();

Inconvénient propre à cette technique : une fois tous les EntityManagerFactory et EntityManager fermés, le gestionnaire de persistance s'arrête, pour redémarrer à la prochaine création d'un EntityManagerFactory. Je suis donc parti dans la création d'une Servlet, qui aura pour mission de créer un EntityManager au démarrage de l'application, et de le libérer à l'arrêt de l'application.

3. Servlet de chargement, et classe PersistenceManager

Avant de créer une Servlet, j'ai d'abord créé une classe, PersistenceManager, chargée de créer des EntityManagerFactory et EntityManager, et de les fermer ensuite.

Le code de cette classe est assez simple :
public class PersistenceManager {
    
    private static EntityManagerFactory emf;
    
    private static EntityManager em;
    
    public static EntityManager getEntityManager() {
        return (em);
    }
    
    public static void loadEntityManager() {
        emf = Persistence.createEntityManagerFactory("AperigeekPU");
        em = emf.createEntityManager();
    }
    
    public static void releaseEntityManager() {
        em.close();
        emf.close();
    }

}

Nous allons maintenant créer une Serlvet, qui devra appeler les méthodes PersistenceManager.loadEntityManager() au chargement de l'application et PersistenceManager.releaseEntityManager() au déchargement :
public class PersistenceLoaderServlet extends GenericServlet {

    @Override
    public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void init() throws ServletException {
        PersistenceManager.loadEntityManager();
    }

    @Override
    public void destroy() {
        PersistenceManager.releaseEntityManager();
    }

}

Suivie d'une déclaration très simple dans web.xml :
<servlet>
    <servlet-name>PersistenceLoaderServlet</servlet-name>
    <servlet-class>com.aperigeek.jpa.servlet.PersistenceLoaderServlet</servlet-class>
    <load-on-startup>2</load-on-startup>
</servlet>

Deux choses sont notables :
  • L'attribut load-on-startup est à 2, pour permettre à la Servlet d'être chargée au déploiement de l'application.
  • Il n'y a aucun mapping pour cette Servlet. Elle n'est en effet pas vouée à être appelée pour répondre à une requête. C'est pour cette même raison que la méthode service() de notre servlet lève une exception.

4. Utilisation de notre EntityManager

Et enfin, dernière étape, utilisation de notre EntityManager. Nous allons devoir :
  • Récupérer nos EntityManagers à la main. N'étant plus gérés par le conteneur, nous n'allons pas pouvoir les injecter en tant que dépendance dans nos beans.
  • Gérer les transactions. Pour la même raison que ci-dessus, les transactions devront être gérées à la main.

Un rapide exemple, pour stocket un User en base de données :
public void addUser(User user) {
    EntityManager em = PersistenceManager.getEntityManager();
    em.getTransaction().begin();
    em.persist(user);
    em.getTransaction().commit();
}

On récupère ici nos EntityManagers grâce à la méthode PersistenceManager.getEntityManager(). Ensuite, avant chaque opération en base de données, nous devons démarrer une transaction (em.getTransaction().begin();), puis commiter les modifications (em.getTransaction().commit();).


Voilà rapidement les étapes à suivre pour utiliser JPA dans un contexte non-géré par le conteneur. Les même étapes peuvent être suivies pour utiliser JPA dans une application Java SE, en modifiant la Servlet par un autre mécanisme permettant de charger notre EntityManager au lancement et de le libérer à la fermeture de l'application.

Permalink  |  Commentaires (0)