JDO 2.3 mit App Engine verwenden

Java Data Objects (JDO) ist eine Standardschnittstelle für den Zugriff auf Datenbanken in Java und bietet eine Zuordnung zwischen Java-Klassen und Datenbanktabellen. Für die Verwendung von JDO mit Datastore steht ein Open-Source-Plug-in zur Verfügung. Mit den Informationen auf dieser Seite soll Ihnen der Einstieg in dieses Plug-in erleichtert werden.

Warnung: Wir sind der Meinung, dass für die meisten Entwickler die Low-Level Datastore API oder eine der speziell für Datastore entwickelten Open Source APIs wie Objectify besser geeignet ist. JDO wurde für die Verwendung mit herkömmlichen relationalen Datenbanken entwickelt und bietet daher keine Möglichkeit, einige Aspekte von Datastore explizit darzustellen, in denen sich Datastore von herkömmlichen relationalen Datenbanken unterscheidet. Hierzu zählen zum Beispiel Entitätengruppen und Ancestor-Abfragen. Dies kann zu subtilen Problemen führen, die schwer zu verstehen und zu beheben sind.

Das App Engine Java SDK enthält eine Implementierung von JDO 2.3 für den App Engine-Datenspeicher. Die Implementierung basiert auf Version 1 der DataNucleus Access Platform, der Open Source-Referenzimplementierung für JDO 2.3.

Hinweis: Die Anleitungen auf dieser Seite beziehen sich auf JDO-Version 2.3, die wiederum Version 1.0 des Plug-ins DataNucleus für App Engine verwendet. App Engine ist jetzt mit dem Plug-in DataNucleus 2.x ausgestattet, mit dem Sie JDO 3.0 ausführen können. Das neue Plug-in unterstützt Beziehungen ohne Abhängigkeiten und bietet neue APIs und Funktionen. Das Upgrade ist mit der vorherigen Version nicht vollständig abwärtskompatibel. Wenn Sie eine Anwendung mit JDO 3.0 neu erstellen, müssen Sie Ihren Code aktualisieren und noch einmal prüfen. Weitere Informationen zur neuen Version finden Sie unter JDO 3.0. Weitere Informationen zum Upgradevorgang finden Sie unter JDO 3.0 mit App Engine verwenden.

JDO 2.3 einrichten

App Engine-Anwendungen setzen für den Zugriff auf den Datenspeicher über JDO Folgendes voraus:

  • Die App Engine-Plug-in-JARs für JDO und DataNucleus müssen sich im Verzeichnis war/WEB-INF/lib/ der Anwendung befinden.
  • Eine Konfigurationsdatei mit dem Namen "jdoconfig.xml" muss im Verzeichnis war/WEB-INF/classes/META-INF/ der Anwendung vorhanden sein, und zwar mit einer Konfiguration, die JDO anweist, den App Engine Datastore zu verwenden.
  • Der Build-Prozess des Projekts muss nach der Kompilierung einen "Optimierungsschritt" für die kompilierten Datenklassen ausführen, um sie der JDO-Implementierung zuzuordnen.

Wenn Sie Ihr Projekt mit Apache Ant erstellen, können Sie zur Durchführung des Optimierungsschritts eine Ant-Aufgabe verwenden, die im SDK enthalten ist. Sie müssen die JARs kopieren und die Konfigurationsdatei erstellen, wenn Sie Ihr Projekt einrichten.

JARs kopieren

Die JDO- und Datastore-JARs sind im App Engine Java-SDK enthalten. Sie können sie im appengine-java-sdk/lib/user/orm/-Verzeichnis finden.

Kopieren Sie die JARs in das Verzeichnis war/WEB-INF/lib/ Ihrer Anwendung.

Achten Sie darauf, dass sich die Datei appengine-api.jar ebenfalls im Verzeichnis war/WEB-INF/lib/ befindet. Möglicherweise haben Sie die Datei bereits beim Erstellen des Projekts kopiert. Das DataNucleus-Plug-in für App Engine greift mithilfe dieser JAR-Datei auf den Datenspeicher zu.

Datei jdoconfig.xml erstellen

Für die JDO-Schnittstelle ist eine Konfigurationsdatei mit dem Namen "jdoconfig.xml" im Verzeichnis war/WEB-INF/classes/META-INF/ der Anwendung erforderlich. Sie können diese Datei direkt in diesem Verzeichnis erstellen oder sie von Ihrem Build-Prozess aus einem Quellverzeichnis an diesen Speicherort kopieren lassen.

Erstellen Sie die Datei mit folgendem Inhalt:

<?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>

Leserichtlinie und Zeitlimit für den Aufruf des Datenspeichers festlegen

Wie auf der Seite Datenspeicherabfragen beschrieben können Sie das Verhalten des Datenspeichers anpassen, indem Sie die Leserichtlinie (Strong Consistency oder Eventual Consistency) und das Aufrufzeitlimit festlegen. In JDO geben Sie hierfür die gewünschten Werte im Element <persistence-manager-factory> der Datei jdoconfig.xml an. Alle mit einer bestimmten PersistenceManager-Instanz ausgeführten Aufrufe verwenden die Konfigurationswerte, die beim Erstellen des Managers durch die PersistenceManagerFactory-Instanz wirksam waren. Sie können diese Einstellungen auch für ein einzelnes Query-Objekt überschreiben.

Zum Festlegen der Leserichtlinie für PersistenceManagerFactory fügen Sie ein Attribut namens "datanucleus.appengine.datastoreReadConsistency" hinzu. Mögliche Werte sind EVENTUAL und STRONG: Wenn Sie nichts angeben, ist der Standardwert STRONG. Diese Einstellungen gelten nur für Ancestor-Abfragen innerhalb einer bestimmten Entitätengruppe. Nicht-Ancestor-Abfragen sind immer Eventual Consistency-Abfragen, ungeachtet der geltenden Leserichtlinie.

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

Sie können für Lese- und Schreibvorgänge jeweils separate Zeitlimits für Datastore-Aufrufe festlegen. Verwenden Sie für Lesevorgänge die JDO-Standardeigenschaft javax.jdo.option.DatastoreReadTimeoutMillis. Verwenden Sie für Schreibvorgänge javax.jdo.option.DatastoreWriteTimeoutMillis. Der Wert ist ein Zeitraum in Millisekunden.

        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />

Wenn Sie gruppenübergreifende Transaktionen (XG-Transaktionen) verwenden möchten, fügen Sie folgende Property hinzu:

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

Sie können mehrere Elemente <persistence-manager-factory> in dieselbe Datei jdoconfig.xml aufnehmen und dabei unterschiedliche name-Attribute verwenden, um PersistenceManager-Instanzen mit verschiedenen Konfigurationen in derselben Anwendung zu nutzen. Die folgende Datei jdoconfig.xml erstellt beispielsweise zwei Konfigurationssätze, einen mit dem Namen ""transactions-optional"" und einen weiteren mit dem Namen ""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>

Informationen zum Erstellen eines PersistenceManager mit einem benannten Konfigurationssatz finden Sie im Abschnitt PersistenceManager-Instanz abrufen weiter unten.

Datenklassen optimieren

JDO bedient sich nach der Kompilierung eines "Optimierungsschritts" im Build-Prozess, um der JDO-Implementierung Datenklassen zuzuordnen.

Wenn Sie Apache Ant verwenden, enthält das SDK eine Ant-Aufgabe, um diesen Schritt durchzuführen.

Sie können den Optimierungsschritt für kompilierte Klassen mit folgendem Befehl über die Befehlszeile ausführen:

java -cp classpath com.google.appengine.tools.enhancer.Enhance
class-files

Der Klassenpfad (classpath) muss die JAR-Datei appengine-tools-api.jar aus dem Verzeichnis appengine-java-sdk/lib/ sowie alle Datenklassen enthalten.

Weitere Informationen zur DataNucleus-Bytecode-Optimierung finden Sie in der DataNucleus-Dokumentation.

PersistenceManager-Instanz abrufen

Eine Anwendung interagiert mit JDO über eine Instanz der PersistenceManager-Klasse. Sie erhalten diese Instanz, indem Sie eine Methode für eine Instanz der PersistenceManagerFactory-Klasse instanziieren und aufrufen. Die Factory erstellt mithilfe der JPO-Konfiguration PersistenceManager-Instanzen.

Da die Initialisierung einer PersistenceManagerFactory-Instanz einige Zeit in Anspruch nimmt, sollte eine Anwendung eine einzelne Instanz wiederverwenden. Damit dies durchgesetzt werden kann, wird eine Ausnahme ausgelöst, wenn die App mehr als eine PersistenceManagerFactory (mit demselben Konfigurationsnamen) instanziiert. Die PersistenceManagerFactory-Instanz lässt sich leicht verwalten, indem Sie eine Singleton-Wrapper-Klasse mit einer statischen Instanz erstellen. Gehen Sie dazu so vor:

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;
    }
}

Tipp: "transactions-optional" bezieht sich auf den Namen der in der Datei jdoconfig.xml festgelegten Konfiguration. Wenn Ihre Anwendung mehrere Konfigurationssets verwendet, müssen Sie diesen Code erweitern, um JDOHelper.getPersistenceManagerFactory() wie gewünscht aufzurufen. Ihr Code sollte eine Singleton-Instanz jeder PersistenceManagerFactory-Instanz im Cache speichern.

Die Anwendung verwendet die Factory-Instanz zum Erstellen einer einzigen PersistenceManager-Instanz für jeden Request, der auf den Datenspeicher zugreift.

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

import PMF;

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

Sie verwenden den PersistenceManager zum Speichern, Aktualisieren und Löschen von Datenobjekten sowie zum Ausführen von Datenspeicherabfragen.

Wenn Sie mit der PersistenceManager-Instanz fertig sind, müssen Sie die zugehörige close()-Methode aufrufen. Verwenden Sie die PersistenceManager-Instanz nach dem Aufrufen ihrer close()-Methode, tritt ein Fehler auf.

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

Nicht unterstützte Features von JDO 2.3

Folgende Funktionen der JDO-Schnittstelle werden nicht von der App Engine-Implementierung unterstützt:

  • Unbeanspruchte ("unowned") Beziehungen: Sie können Beziehungen ohne Abhängigkeiten mit expliziten Schlüsselwerten implementieren. Die JDO-Syntax für Beziehungen ohne Abhängigkeiten wird gegebenenfalls in einer zukünftigen Version unterstützt.
  • N-zu-N-Beziehungen mit Abhängigkeiten
  • "Join"-Abfragen: Wenn Sie eine Abfrage für einen übergeordneten Entitätstyp ausführen, können Sie kein Feld einer untergeordneten Entität in einem Filter verwenden. Sie können jedoch das Beziehungsfeld der übergeordneten Entität mithilfe eines Schlüssels direkt in einer Abfrage testen.
  • JDOQL-Gruppierung und andere Aggregatabfragen
  • Polymorphe Abfragen: Sie können keine Abfrage einer Klasse ausführen, um Instanzen einer abgeleiteten Klasse abzurufen. Jede Klasse wird durch einen eigenen Entitätstyp im Datenspeicher dargestellt.
  • IdentityType.DATASTORE für die Annotation @PersistenceCapable. Nur IdentityType.APPLICATION wird unterstützt.
  • Derzeit verhindert ein Programmfehler 1:n-Beziehungen mit Abhängigkeiten ("owned"), bei denen das übergeordnete und das untergeordnete Element zur selben Klasse gehören, was die Modellierung von Baumstrukturen erschwert. Dieser Fehler wird in einer zukünftigen Version behoben. Sie können das Problem umgehen, indem Sie entweder für das übergeordnete Element oder für die untergeordneten Elemente explizite Schlüsselwerte hinterlegen.

Transaktionen deaktivieren und vorhandene JDO-Anwendungen portieren

In der von uns empfohlenen JDO-Konfiguration wird ein Attribut namens "datanucleus.appengine.autoCreateDatastoreTxns" auf true festgelegt. Dies ist eine App Engine-spezifische Property, die in der JDO-Implementierung eine Zuordnung zwischen Datenspeichertransaktionen und den im Anwendungscode verwalteten JDO-Transaktionen bewirkt. Wenn Sie eine neue Anwendung von Grund auf erstellen, ist das vermutlich in Ihrem Sinne. Wenn Sie jedoch bereits über eine JDO-basierte Anwendung verfügen, die Sie auf App Engine ausführen möchten, ist es eventuell besser, eine alternative Persistenzkonfiguration zu verwenden, die den Wert dieser Eigenschaft auf false setzt:

<?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>

Die Nützlichkeit dieser Vorgehensweise erklärt sich aus der Tatsache, dass Sie innerhalb einer Transaktion nur für Objekte, die zur selben Entitätengruppe gehören, Vorgänge ausführen können. Anwendungen, die mit herkömmlichen Datenbanken erstellt wurden, gehen in der Regel von der Verfügbarkeit globaler Transaktionen aus, die die Aktualisierung beliebiger Gruppen von Datensätzen innerhalb einer Transaktion ermöglichen. Da der App Engine-Datenspeicher keine globalen Transaktionen unterstützt, löst App Engine Ausnahmen aus, wenn Ihr Code von der Verfügbarkeit globaler Transaktionen ausgeht. Anstatt Ihre (möglicherweise große) Codebasis durchzugehen und sämtlichen Transaktionsverwaltungscode zu entfernen, können Sie einfach Datenspeichertransaktionen deaktivieren. Dies ändert nichts an den Annahmen, die Ihr Code in Bezug auf die Atomarität von Änderungen an mehreren Datensätzen trifft, es ermöglicht Ihnen jedoch, Ihre Anwendung funktionsfähig zu machen. So können Sie sich anschließend darauf konzentrieren, Transaktionscode nach Bedarf inkrementell zu refaktorieren, anstatt alle Vorgänge auf einmal durchzuführen.