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 du datastore doivent se trouver dans le répertoire war/WEB-INF/lib/ de l'application.
  • Un fichier de configuration nommé persistence.xml, indiquant à JPA d'utiliser le datastore App Engine, doit être stocké dans le répertoire war/WEB-INF/classes/META-INF/ de l'application.
  • 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.

Copie des fichiers JAR

Les fichiers JAR de JPA et du magasin de données sont disponibles dans le SDK Java App Engine. Ils sont stockés 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 le fichier 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éer le fichier persistence.xml

L'interface JPA requiert la présence d'un fichier de configuration nommé persistence.xml dans le répertoire de l'application war/WEB-INF/classes/META-INF/. 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 indiqué sur la page Requêtes Datastore, vous pouvez définir les règles de lecture (cohérence forte ou cohérence à terme) et le délai d'appel du datastore pour EntityManagerFactory dans le fichier persistence.xml. Ces paramètres sont stockés dans l'élément <persistence-unit>. Tous les appels effectués avec une instance EntityManager donnée utilisent la configuration sélectionnée au moment de la création du gestionnaire par la classe EntityManagerFactory. Vous pouvez également ignorer ces options pour un objet Query spécifique (comme décrit ci-après).

Pour définir les règles de lecture, spécifiez 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 datastore 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 entre groupes (XG), ajoutez la propriété suivante :

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

Vous pouvez avoir dans le même fichier persistence.xml plusieurs éléments <persistence-unit> utilisant différents attributs name, afin d'exploiter des instances EntityManager ayant des configurations différentes dans la même application. Par exemple, le fichier persistence.xml suivant établit deux ensembles de configuration, l'un nommé "transactions-optional" et l'autre nommé "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 passer outre les règles de lecture et le délai d'appel pour un objet Query spécifique. Pour ignorer les règles de lecture d'un objet Query, appelez sa méthode setHint() à l'aide du code suivant :

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

Comme indiqué ci-dessus, les valeurs possibles de cette règle sont "EVENTUAL" et "STRONG".

Pour passer outre le délai de lecture, appelez la méthode à l'aide du code setHint() suivant :

        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.

Pour effectuer l'opération d'enrichissement sur des classes compilées, exécutez la commande ci-après à partir de la ligne de commande :

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

Le paramètre classpath doit inclure les fichiers JAR datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar et geronimo-jpa-*.jar (où * correspond au numéro de version approprié de chaque fichier JAR) situés dans le répertoire appengine-java-sdk/lib/tools/, ainsi que l'ensemble de vos classes de données.

Pour en savoir plus sur l'outil d'enrichissement de bytecode DataNucleus, consultez la documentation de 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é qu'une instance EntityManagerFactory met du temps à s'initialiser, il est judicieux de réutiliser une même instance autant 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 défini dans le fichier persistence.xml. Si votre application utilise plusieurs ensembles de configuration, vous devrez étendre ce code pour qu'il appelle la méthode Persistence.createEntityManagerFactory() autant de fois que nécessaire. Votre code doit mettre en cache une instance singleton 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 datastore.

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

import EMF;

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

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

Lorsque vous avez fini d'utiliser l'instance EntityManager, vous devez appeler sa méthode close(). Vous ne devez pas utiliser l'instance EntityManager 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 datastore à l'aide de JPA, attribuez-lui 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 (par exemple, java.util.List<...>) de valeurs d'un type 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, ainsi qu'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 ? OK. 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 classe Worker. Cela indique à JPA de stocker tous les champs persistants de la classe Worker dans les entités du datastore correspondant à ses sous-classes. L'entité du datastore créée à la suite de l'appel de persist() avec une instance Employee aura deux propriétés nommées "department" "et "salary". L'entité du datastore créée à la suite de l'appel de persist() avec 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 instances Employee et Intern, nous souhaitions également disposer d'une spécialisation de l'instance Employee décrivant les employés qui ont quitté l'entreprise :

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 classe FormerEmployee en définissant son attribut strategy sur InheritanceType.TABLE_PER_CLASS. Cette opération indique à JPA de stocker tous les champs persistants de la classe FormerEmployee et de ses super-classes dans les entités du datastore correspondant aux instances FormerEmployee. L'entité du datastore créée à la suite de l'appel de persist() avec 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" qui correspond à 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.