Utiliser JPA avec App Engine

Java Persistence API (JPA) est une interface standard permettant d'accéder à des bases de données en Java. Elle fournit un mappage automatique entre les classes Java et les tables de base de données. Un plug-in Open Source est disponible pour utiliser JPA avec Datastore. Cette page explique comment faire vos premiers pas avec ce plug-in.

Avertissement : Nous pensons que la plupart des développeurs pourront améliorer leur expérience en tirant parti de l'API Datastore de bas niveau ou de l'une des API Open Source développées spécifiquement pour Datastore, comme Objectify. JPA a été conçue pour être utilisée avec des bases de données classiques et n'a donc aucun moyen de représenter explicitement certains des aspects qui différencient Datastore des bases de données relationnelles, tels que les groupes d'entités et les requêtes ascendantes. Cela peut entraîner des problèmes subtils, difficiles à comprendre et à résoudre.

La version 1.x du plug-in est incluse dans le SDK Java d'App Engine, qui met en œuvre JPA version 1.0. La mise en œuvre est basée sur la version 1.1 de la plate-forme d'accès DataNucleus.

Remarque : Les instructions de cette page s'appliquent à JPA version 1, qui utilise la version 1.x du plug-in DataNucleus pour App Engine. La version 2.x du plug-in DataNucleus est également disponible, ce qui vous permet d'utiliser JPA 2.0. Le plug-in 2.x fournit un certain nombre de nouvelles API et fonctionnalités. Cependant, cette mise à niveau n'est pas totalement rétrocompatible avec la version 1.x. Si vous recompilez une application à l'aide de JPA 3.0, vous devez mettre à jour votre code et le tester de nouveau. Pour en savoir plus sur cette nouvelle version, consultez la page Utiliser JPA 2.0 avec App Engine.

Configurer JPA

Pour accéder au magasin de données au moyen de JPA, une application App Engine a besoin que les conditions suivantes soient remplies :

  • Les fichiers JAR de JPA et de Datastore doivent figurer dans le répertoire war/WEB-INF/lib/ de l'application.
  • Un fichier de configuration nommé persistence.xml doit figurer dans le répertoire war/WEB-INF/classes/META-INF/ de l'application, avec une configuration qui indique à JPA d'utiliser le magasin de données d'App Engine.
  • Le processus de compilation du projet doit exécuter une étape "d'enrichissement" post-compilation sur les classes de données compilées pour les associer à la mise en œuvre JPA.

Si vous compilez votre projet à l'aide d'Apache Ant, vous pouvez utiliser une tâche Ant incluse dans le SDK pour exécuter la phase d'enrichissement du code. Vous devez copier les fichiers JAR et créer le fichier de configuration au moment où vous configurez votre projet.

Copier des fichiers JAR

Les fichiers JAR de JPA et du datastore sont inclus dans le SDK Java d'App Engine. Vous pouvez les trouver dans le répertoire appengine-java-sdk/lib/user/orm/.

Copiez les fichiers JAR dans le répertoire war/WEB-INF/lib/ de votre application.

Assurez-vous que appengine-api.jar se trouve également dans le répertoire war/WEB-INF/lib/. (Vous l'avez peut-être déjà copié au moment de la création du projet.) Le plug-in DataNucleus App Engine utilise ce fichier JAR pour accéder au magasin de données.

Création du fichier persistence.xml

L'interface JPA requiert un fichier de configuration nommé persistence.xml dans le répertoire war/WEB-INF/classes/META-INF/ de l'application. Vous pouvez créer directement ce fichier à cet emplacement, ou paramétrer votre processus de compilation afin de copier ce fichier à partir d'un répertoire source.

Créez le fichier en y incluant le contenu suivant :

<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

</persistence>

Règles de lecture et durée maximale de l'appel au Datastore

Comme décrit sur la page Requêtes Datastore, vous pouvez définir les règles de lecture (cohérence forte ou à terme) et la durée maximale de l'appel au datastore pour une instance EntityManagerFactory dans le fichier persistence.xml. Ces paramètres sont définis dans l'élément <persistence-unit>. Tous les appels passés avec une instance EntityManager donnée utiliseront la configuration en vigueur lors de la création du gestionnaire par EntityManagerFactory. Vous pouvez également ignorer ces options pour un objet Query spécifique (comme décrit ci-dessous).

Pour définir les règles de lecture, incluez une propriété nommée datanucleus.appengine.datastoreReadConsistency. Ses valeurs possibles sont EVENTUAL (pour les lectures avec cohérence à terme) et STRONG (pour les lectures avec cohérence forte). Si aucune valeur n'est spécifiée, la valeur par défaut de cet argument est STRONG.

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

Le délai d'appel vers le magasin de données que vous définissez pour les lectures peut différer de celui défini pour les écritures. Pour les lectures, utilisez la propriété standard JPA javax.persistence.query.timeout. Pour les écritures, utilisez datanucleus.datastoreWriteTimeout. La valeur du délai d'appel correspond à une durée exprimée en millisecondes.

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />

Si vous souhaitez utiliser des transactions (XG) entre les groupes, ajoutez la propriété suivante :

            <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

Vous pouvez avoir plusieurs éléments <persistence-unit> dans le même fichier persistence.xml, utilisant différents attributs name pour utiliser des instances EntityManager avec différentes configurations dans la même application. Par exemple, le fichier persistence.xml suivant établit deux configurations, l'une nommée "transactions-optional" et l'autre "eventual-reads-short-deadlines" :

<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="eventual-reads-short-deadlines">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />
        </properties>
    </persistence-unit>
</persistence>

Pour en savoir plus sur la création d'une instance EntityManager avec un ensemble de configuration nommé, consultez la section Obtenir une instance EntityManager ci-dessous.

Vous pouvez ignorer les règles de lecture et la durée maximale de l'appel pour un objet Query spécifique. Pour ignorer les règles de lecture pour un Query, appelez sa méthode setHint() comme indiqué ici :

        Query q = em.createQuery("select from " + Book.class.getName());
        q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

Comme indiqué précédemment, les valeurs possibles sont "EVENTUAL" et "STRONG".

Pour ignorer le délai de lecture, appelez setHint() comme ci-dessous :

        q.setHint("javax.persistence.query.timeout", 3000);

Il n'existe aucun moyen d'ignorer la configuration de ces options lorsque vous récupérez des entités par leur clé.

Enrichissement du code des classes de données

La mise en œuvre DataNucleus de JPA ajoute au processus de compilation une étape "d'enrichissement" post-compilation, afin d'associer des classes de données à la mise en œuvre de JPA.

Si vous utilisez Apache Ant, le SDK intègre une tâche Ant permettant d'effectuer cette opération.

Pour appliquer l'opération d'enrichissement du code aux classes compilées, exécutez la commande ci-dessous depuis la ligne de commande :

java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer
class-files

Le paramètre classpath doit contenir les fichiers JAR datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar et geronimo-jpa-*.jar (où * représente le numéro de version adéquat pour chaque fichier JAR) du répertoire appengine-java-sdk/lib/tools/, ainsi que toutes vos classes de données.

Pour plus d'informations sur l'outil d'enrichissement de bytecode DataNucleus, consultez la documentation relative à DataNucleus.

Obtenir une instance EntityManager

Une application interagit avec JPA à l'aide d'une instance de la classe EntityManager. Pour récupérer cette instance, vous devez instancier la classe EntityManagerFactory et appeler une méthode sur cet objet. La fabrique utilise la configuration JPA (identifiée par le nom "transactions-optional") pour créer des instances EntityManager.

Étant donné que l'initialisation d'une instance EntityManagerFactory demande un certain temps, il est recommandé de réutiliser cette dernière autant de fois que possible. Un moyen simple d'effectuer cette opération consiste à créer une classe wrapper singleton avec une instance statique à l'aide du code suivant :

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

Conseil : "transactions-optional" fait référence au nom de l'ensemble de configuration figurant dans le fichier persistence.xml. Si votre application utilise plusieurs configurations, vous devrez étendre ce code pour appeler Persistence.createEntityManagerFactory() à votre guise. Votre code doit mettre en cache une instance unique de chaque EntityManagerFactory.

L'application utilise l'instance de fabrique afin de créer une instance EntityManager pour chaque requête qui accède au magasin de données.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import EMF;

// ...
    EntityManager em = EMF.get().createEntityManager();

Vous utilisez l'instance EntityManager pour stocker, mettre à jour et supprimer des objets de données, ainsi que pour exécuter des requêtes de magasin de données.

Dès que vous avez terminé avec l'instance EntityManager, appelez sa méthode close(). L'instance EntityManager ne doit pas être utilisée après avoir appelé sa méthode close().

    try {
        // ... do stuff with em ...
    } finally {
        em.close();
    }

Annotations de classe et de champ

Chaque objet enregistré par JPA devient une entité du magasin de données App Engine. Le genre de l'entité est dérivé du nom simple de la classe (sans le nom du package). Chaque champ persistant de la classe représente une propriété de l'entité, dont le nom correspond au nom du champ (en respectant la distinction majuscules-minuscules).

Pour indiquer qu'une classe Java peut être stockée et extraite du magasin de données à l'aide de JPA, associez à cette classe une annotation @Entity. Exemple :

import javax.persistence.Entity;

@Entity
public class Employee {
    // ...
}

Les champs de la classe de données qui doivent être stockés dans le magasin de données doivent être d'un type persistant par défaut ou être explicitement déclarés comme persistants. Vous trouverez un graphique détaillant le comportement de persistance par défaut de JPA sur le site Web de DataNucleus. Pour déclarer explicitement un champ comme persistant, attribuez-lui une annotation @Basic :

import java.util.Date;
import javax.persistence.Enumerated;

import com.google.appengine.api.datastore.ShortBlob;

// ...
    @Basic
    private ShortBlob data;

Les types de champs possibles sont répertoriés ci-dessous :

  • L'un des types de base pris en charge par le magasin de données
  • Une collection (telle qu'un objet java.util.List<...>) de valeurs de l'un des types de datastore de base
  • Une instance ou une collection d'instances d'une classe @Entity
  • Une classe encapsulée, stockée sous forme de propriétés dans l'entité

Une classe de données doit disposer d'un constructeur par défaut public ou protégé, ainsi que d'un champ dédié au stockage de la clé primaire de l'entité correspondante dans le magasin de données. Vous pouvez choisir parmi quatre genres de champs de clé, chacun utilisant un type de valeur et des annotations distincts. (Pour plus d'informations, consultez la page Créer des données : clés.) Le champ de clé le plus simple correspond à un entier long qui est automatiquement renseigné par JPA avec une valeur unique pour l'ensemble des instances de la classe lorsque l'objet est enregistré pour la première fois dans le magasin de données. Les clés de type entier long utilisent une annotation @Id et une annotation @GeneratedValue(strategy = GenerationType.IDENTITY) :

import com.google.appengine.api.datastore.Key;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

Voici un exemple de classe de données :

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String firstName;

    private String lastName;

    private Date hireDate;

    // Accessors for the fields. JPA doesn't use these, but your application
    does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

Héritage

JPA prend en charge la création de classes de données qui utilisent des stratégies d'héritage. Avant d'aborder le fonctionnement de l'héritage JPA dans App Engine, nous vous recommandons de consulter la documentation DataNucleus sur ce sujet, puis de reprendre votre lecture de cette documentation. C'est fait ? Bien. L'héritage JPA dans App Engine fonctionne comme décrit dans la documentation de DataNucleus, avec quelques restrictions supplémentaires. Cette section spécifie ces restrictions, puis en présente quelques exemples concrets.

La stratégie d'héritage "JOINED" (par jointure) vous permet de fractionner les données d'un même objet de données dans différentes "tables". Toutefois, du fait que le magasin de données d'App Engine n'est pas compatible avec les jointures, l'utilisation d'un objet de données avec cette stratégie d'héritage nécessite un appel de procédure à distance pour chaque niveau d'héritage. C'est un fonctionnement potentiellement très inefficace. Par conséquent, la stratégie d'héritage "JOINED" n'est pas compatible avec les classes de données.

Par ailleurs, la stratégie d'héritage "SINGLE_TABLE" (table unique) vous permet de stocker les données d'un objet de données dans une "table" unique associée à la classe persistante figurant à la racine de votre hiérarchie d'héritage. Bien que cette stratégie ne présente pas de risque d'inefficacité, elle n'est pas prise en charge à ce jour. Cet aspect sera éventuellement modifié dans les versions ultérieures.

Maintenant, voici la bonne nouvelle : les stratégies "TABLE_PER_CLASS" et "MAPPED_SUPERCLASS" fonctionnent comme décrit dans la documentation de DataNucleus. Voyons un exemple :

Worker.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@Entity
@MappedSuperclass
public abstract class Worker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String department;
}

Employee.java

// ... imports ...

@Entity
public class Employee extends Worker {
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@Entity
public class Intern extends Worker {
    private Date internshipEndDate;
}

Dans cet exemple, nous avons ajouté une annotation @MappedSuperclass à la déclaration de la classe Worker. Cela indique à JPA de stocker tous les champs persistants de la classe Worker dans les entités du magasin de données correspondant à ses sous-classes. L'entité du magasin de données créée à la suite de l'appel de persist() sur une instance Employee possède deux propriétés nommées "department" et "salary". L'entité du magasin de données créée à la suite de l'appel de persist() sur une instance Intern possède deux propriétés nommées "department" et "internshipEndDate". Le magasin de données ne contient aucune entité du genre "Worker".

Passons maintenant à un aspect un peu plus intéressant. Supposons qu’en plus des classes Employee et Intern, nous souhaitons également disposer d'une spécialisation de la classe Employee représentant les employés qui ont quitté la société :

FormerEmployee.java

import java.util.Date;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
// ... imports ...

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FormerEmployee extends Employee {
    private Date lastDay;
}

Dans cet exemple, nous avons ajouté une annotation @Inheritance à la déclaration de la classe FormerEmployee, avec un attribut strategy défini sur InheritanceType.TABLE_PER_CLASS. Cela indique à JPA de stocker tous les champs persistants de la classe FormerEmployee et de ses super-classes dans les entités de datastore correspondant aux instances FormerEmployee. L'entité du magasin de données créée à la suite de l'appel de persist() sur une instance FormerEmployee possède trois propriétés nommées "department", "salary" et "lastDay". Il n'y aura jamais d'entité du genre "Employee" correspondant à un FormerEmployee mais, si vous appelez persist() avec un objet dont le type d'exécution est Employee, vous créez une entité de type "Employee".

L'utilisation combinée de relations et de stratégies d'héritage fonctionne tant que les types déclarés de vos champs de relations correspondent aux types d'exécution des objets que vous attribuez à ces champs. Pour plus d'informations, reportez-vous à la section sur les relations polymorphes. Bien que cette section fournisse des exemples relatifs à JDO, les concepts et les restrictions qui s'appliquent à JDO sont les mêmes que pour JPA.

Fonctionnalités non compatibles de JPA 1.0

Les fonctionnalités de l'interface JPA non prises en charge par l'implémentation App Engine sont les suivantes :

  • Relations d'appartenance plusieurs-à-plusieurs et relations indépendantes. Vous pouvez mettre en œuvre des relations indépendantes à l'aide de valeurs Key explicites, bien que la vérification du type ne soit pas appliquée dans l'API.
  • Requêtes de type "jointure". Vous ne pouvez pas utiliser un champ d'une entité enfant dans un filtre lorsque vous exécutez une requête sur le genre parent. Notez que vous pouvez tester directement le champ de relation du parent dans une requête à l'aide d'une clé.
  • Requêtes d'agrégation (GROUP BY, HAVING, SUM, AVG, MAX, MIN).
  • Requêtes polymorphes. Vous ne pouvez pas exécuter de requête d'une classe pour récupérer des instances d'une sous-classe. Chaque classe est représentée par un genre d'entité distinct dans le datastore.
Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java