Utilisation de JDO 2.3 avec App Engine

JDO (Java Data Objects) est une interface standard permettant d’accéder aux bases de données en Java. Elle permet le mappage entre les classes et les tables de base de données Java. Il existe un plug-in open-source permettant d'utiliser JDO avec Datastore. Ici, vous découvrirez comment l'utiliser.

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. JDO a été conçu pour être utilisé avec des bases de données relationnelles traditionnelles et ne peut donc pas représenter explicitement certains aspects de Datastore qui le différencient des bases de données relationnelles, comme les groupes d'entités et les requêtes ascendantes. Cela peut entraîner des problèmes subtils difficiles à comprendre et à résoudre.

Le SDK Java App Engine intègre une mise en œuvre de JDO 2.3 pour App Engine Datastore. La mise en œuvre repose sur la version 1.0 de la plate-forme d'accès DataNucleus, qui est la mise en œuvre de référence Open Source pour JDO 2.3.

Remarque : Les instructions de cette page s'appliquent à JDO version 2.3, qui utilise la version 1.0 du plug-in DataNucleus pour App Engine. App Engine propose désormais le plug-in DataNucleus 2.x qui permet d'exécuter JDO 3.0. Le nouveau plug-in prend en charge les relations indépendantes et propose un certain nombre de nouvelles API et fonctionnalités. Cette mise à niveau n'est pas totalement rétrocompatible avec la version précédente. Si vous recompilez une application à l'aide de JDO 3.0, vous devez mettre à jour et retester votre code. Pour plus d'informations sur la nouvelle version, consultez la section JDO 3.0. Pour plus d'informations sur la mise à niveau, consultez la section Utiliser JDO 3.0 avec App Engine.

Configurer JDO

Pour accéder au datastore au moyen de JDO, une application App Engine a besoin que les conditions suivantes soient remplies :

  • Les fichiers JAR de JDO et du plug-in DataNucleus App Engine doivent se trouver dans le répertoire war/WEB-INF/lib/ de l'application.
  • Un fichier de configuration nommé jdoconfig.xml, indiquant à JDO d'utiliser l'App Engine Datastore, doit être stocké dans le répertoire de l'application war/WEB-INF/classes/META-INF/.
  • 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 JDO.

Copie des fichiers JAR

Les fichiers JAR de JDO 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 datastore.

Création du fichier jdoconfig.xml

L'interface JDO requiert la présence d'un fichier de configuration nommé jdoconfig.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"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>
</jdoconfig>

Définir les règles de lecture et la durée maximale de l'appel au datastore

Comme décrit sur la page Requêtes Datastore, vous pouvez personnaliser le comportement du Datastore en définissant les règles de lecture (cohérence forte ou à terme) et la durée maximale de l'appel. Pour ce faire, dans JDO, spécifiez les valeurs souhaitées dans l'élément <persistence-manager-factory> du fichier jdoconfig.xml. Tous les appels passés avec une instance PersistenceManager donnée utilisent les valeurs de configuration en vigueur lors de la création du gestionnaire par PersistenceManagerFactory. Vous pouvez également ignorer ces paramètres pour un objet Query unique.

Pour définir la règle de lecture pour une classe PersistenceManagerFactory, spécifiez une propriété nommée datanucleus.appengine.datastoreReadConsistency. Les valeurs possibles sont EVENTUAL et STRONG. Si aucune valeur n'est spécifiée, la valeur par défaut est STRONG. Notez que ces paramètres s'appliquent uniquement aux requêtes ascendantes d'un groupe d'entités donné. Les requêtes non-ascendantes et entre les groupes finissent toujours par être cohérentes, quelle que soit la règle de lecture en vigueur.

        <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 JDO javax.jdo.option.DatastoreReadTimeoutMillis. Pour les écritures, utilisez javax.jdo.option.DatastoreWriteTimeoutMillis. Cette valeur correspond à une durée exprimée en millisecondes.

        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" 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 plusieurs éléments <persistence-manager-factory> dans le même fichier jdoconfig.xml, en utilisant différents attributs name, pour utiliser des instances PersistenceManager avec des configurations différentes dans la même application. Par exemple, le fichier jdoconfig.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"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>

    <persistence-manager-factory name="eventual-reads-short-deadlines">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>

        <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />
    </persistence-manager-factory>
</jdoconfig>

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

Enrichissement du code des classes de données

JDO utilise une phase "d'enrichissement" post-compilation dans le processus de compilation pour associer des classes de données à l'implémentation JDO.

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 com.google.appengine.tools.enhancer.Enhance
class-files

Le paramètre classpath doit inclure le fichier JAR appengine-tools-api.jar situé dans le répertoire appengine-java-sdk/lib/, 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 PersistenceManager

Une application interagit avec JDO à l'aide d'une instance de la classe PersistenceManager. Pour récupérer cette instance, vous devez instancier et appeler une méthode sur une instance de la classe PersistenceManagerFactory. L'usine utilise la configuration JDO pour créer des instances PersistenceManager.

Étant donné que l'initialisation d'une instance PersistenceManagerFactory demande un certain temps, une application doit réutiliser une instance. Pour appliquer cette règle, une exception est levée si l'application instancie plus d'une instance PersistenceManagerFactory (avec le même nom de configuration). Un moyen simple de gérer l'instance PersistenceManagerFactory consiste à créer une classe wrapper unique avec une instance statique à l'aide du code suivant :

PMF.java

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
    private static final PersistenceManagerFactory pmfInstance =
        JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

Conseil : "transactions-optional" fait référence au nom de l'ensemble de configuration défini dans le fichier jdoconfig.xml. Si votre application utilise plusieurs ensembles de configuration, vous devrez étendre ce code pour qu'il appelle la méthode JDOHelper.getPersistenceManagerFactory() autant de fois que nécessaire. Votre code doit mettre en cache une instance singleton de chaque PersistenceManagerFactory.

L'application utilise l'instance d'usine afin de créer une instance PersistenceManager pour chaque requête qui accède au datastore.

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

import PMF;

// ...
    PersistenceManager pm = PMF.get().getPersistenceManager();

Utilisez l'instance PersistenceManager 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 PersistenceManager, vous devez appeler sa méthode close(). Vous ne devez pas utiliser l'instance PersistenceManager après avoir appelé sa méthode close().

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

Fonctionnalités incompatibles de JDO 2.3

Les fonctionnalités de l'interface JDO non prises en charge par la mise en œuvre d'App Engine sont les suivantes :

  • Relations indépendantes. Vous pouvez mettre en œuvre des relations indépendantes à l'aide de valeurs Key explicites. La syntaxe de JDO relative aux relations indépendantes sera éventuellement prise en charge dans une version ultérieure.
  • Relations d'appartenance plusieurs à plusieurs.
  • Requêtes "Join". Vous ne pouvez pas utiliser le 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é.
  • Groupement JDOQL et autres requêtes d'agrégation.
  • 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.
  • IdentityType.DATASTORE pour l'annotation @PersistenceCapable. Seule la fonctionnalité IdentityType.APPLICATION est prise en charge.
  • À ce jour, un bug empêche les relations d'appartenance un à plusieurs dans lesquelles le parent et l'enfant correspondent à la même classe, ce qui complique la modélisation des arborescences. Ce problème sera résolu dans une version ultérieure. Vous pouvez contourner cette difficulté en stockant des valeurs Key explicites pour le parent ou pour les enfants.

Désactiver des transactions et porter des applications JDO existantes

La configuration JDO que nous recommandons d'utiliser définit une propriété nommée datanucleus.appengine.autoCreateDatastoreTxns sur true. Cette propriété, propre à App Engine, indique à la mise en œuvre de JDO d'associer les transactions datastore aux transactions JDO qui sont gérées dans le code de l'application. Si vous compilez une nouvelle application dans son intégralité, vous cherchez probablement à obtenir ce résultat. Toutefois, si vous disposez déjà d'une application reposant sur JDO que vous voulez exécuter sur App Engine, il est possible que vous souhaitiez utiliser une autre configuration de persistance qui définit la valeur de cette propriété sur false :

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="false"/>
    </persistence-manager-factory>
</jdoconfig>

Pour comprendre l'utilité de cette opération, rappelez-vous qu'au sein d'une transaction, vous ne pouvez agir que sur des objets qui appartiennent au même groupe d'entités. Les applications compilées à l'aide de bases de données traditionnelles supposent généralement que les transactions globales sont disponibles, ce qui vous permet de mettre à jour n'importe quel ensemble d'enregistrements dans le cadre d'une transaction. Le datastore AppEngine ne prend pas en charge les transactions globales. Par conséquent, App Engine lève des exceptions si votre code repose sur la disponibilité de ces transactions. Plutôt que de parcourir votre codebase (potentiellement volumineux) et de supprimer la totalité du code spécifique à la gestion des transactions, vous pouvez simplement désactiver les transactions du datastore. Cette solution ne résout pas le problème des suppositions de votre code concernant l'atomicité des modifications d'enregistrements multiples, mais elle vous permet de faire fonctionner votre application tout en vous laissant vous concentrer sur le réusinage de votre code transactionnel, de manière incrémentielle et selon les besoins, plutôt qu'en une seule fois.