JPA mit App Engine verwenden

Die Java Persistence API (JPA) ist eine Standardschnittstelle für den Zugriff auf Datenbanken in Java und bietet eine automatische Zuordnung zwischen Java-Klassen und Datenbanktabellen. Für die Verwendung von JPA 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. JPA 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 komplexen Problemen führen, die schwer zu verstehen und zu beheben sind.

Version 1.x des Plug-ins ist im App Engine Java SDK enthalten, in dem JPA Version 1.0 implementiert ist. Die Implementierung basiert auf DataNucleus Access Platform Version 1.1.

Hinweis: Die Anleitungen auf dieser Seite beziehen sich auf die JPA Version 1, die Version 1.x des DataNucleus-Plug-ins für App Engine nutzt. Version 2.x des DataNucleus-Plug-ins ist ebenfalls verfügbar. Damit können Sie JPA 2.0 verwenden. Das 2.x-Plug-in bietet eine Reihe neuer APIs und Funktionen. Das Upgrade ist jedoch nicht vollständig abwärtskompatibel zur Version 1.x. Wenn Sie eine Anwendung mit JPA 2.0 neu erstellen, müssen Sie Ihren Code aktualisieren und noch einmal testen. Weitere Informationen zur neuen Version finden Sie unter JPA 2.0 mit App Engine verwenden.

Einrichten von JPA

App Engine-Anwendungen benötigen folgende Elemente, um JPA für den Zugriff auf den Datenspeicher zu verwenden:

  • Die JPA- und Datenspeicher-JARs müssen im Verzeichnis war/WEB-INF/lib/ der Anwendung vorhanden sein.
  • Eine Konfigurationsdatei mit dem Namen "persistence.xml" muss im Verzeichnis war/WEB-INF/classes/META-INF/ der Anwendung vorhanden sein, und zwar mit einer Konfiguration, die JPA anweist, den App Engine Datastore zu verwenden.
  • Beim Erstellungsprozess des Projekts muss nach der Kompilierung ein "Optimierungsschritt" für die kompilierten Datenklassen ausgeführt werden, um sie der JPA-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 JPA- und Datenspeicher-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 persistence.xml erstellen

Für die JPA-Schnittstelle ist eine Konfigurationsdatei mit dem Namen "persistence.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 Erstellungsprozess aus einem Quellverzeichnis in dieses Verzeichnis kopieren lassen.

Erstellen Sie die Datei mit folgendem Inhalt:

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

Leserichtlinie und Zeitlimit für den Aufruf des Datenspeichers

Wie auf der Seite Datastore-Abfragen beschrieben, können Sie die Leserichtlinie (Strong Consistency gegenüber Eventual Consistency) und das Zeitlimit für den Datenspeicheraufruf für eine EntityManagerFactory in der Datei persistence.xml festlegen. Diese Einstellungen werden in das <persistence-unit>-Element aufgenommen. Alle mit einer bestimmten EntityManager-Instanz ausgeführten Aufrufe verwenden die Konfiguration, die beim Erstellen des Managers durch die EntityManagerFactory ausgewählt wurde. Sie können diese Optionen auch für eine einzelne Query überschreiben (unten beschrieben).

Nehmen Sie zur Festlegung der Leserichtlinie das Attribut mit dem Namen "datanucleus.appengine.datastoreReadConsistency" mit auf. Die möglichen Werte sind EVENTUAL (für Lesevorgänge mit Eventual Consistency) und STRONG (für Lesevorgänge mit Strong Consistency). Wenn keine Angabe erfolgt, lautet der Standardwert STRONG.

            <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 das JPA-Standardattribut javax.persistence.query.timeout. Verwenden Sie für Schreibvorgänge datanucleus.datastoreWriteTimeout. Der Wert ist ein Zeitraum in Millisekunden.

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" 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-unit> in dieselbe Datei persistence.xml aufnehmen und dabei unterschiedliche name-Attribute verwenden, um EntityManager-Instanzen mit verschiedenen Konfigurationen in derselben Anwendung zu nutzen. Die folgende Datei persistence.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" ?>
<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>

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

Sie können die Leserichtlinie und das Zeitlimit für den Aufruf eines einzelnen Query-Objekts überschreiben. Rufen Sie zum Überschreiben der Leserichtlinien für ein Query die zugehörige setHint()-Methode so auf:

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

Wie oben erwähnt, sind die möglichen Werte "EVENTUAL" und "STRONG".

Wenn Sie das Lesezeitlimit überschreiben möchten, rufen Sie wie setHint()so auf:

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

Es gibt keine Möglichkeit zum Überschreiben der Konfiguration für diese Optionen, wenn Sie Entitäten anhand ihres Schlüssels abrufen.

Datenklassen optimieren

Die DataNucleus-Implementierung von JPA bedient sich nach der Kompilierung eines "Optimierungsschritts" im Erstellungsprozess, um der JPA-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 org.datanucleus.enhancer.DataNucleusEnhancer
class-files

Der Klassenpfad muss die JARs datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar und geronimo-jpa-*.jar – dabei steht * für die entsprechende Versionsnummer der einzelnen JAR-Dateien – aus dem appengine-java-sdk/lib/tools/-Verzeichnis sowie sämtliche Datenklassen enthalten.

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

EntityManager-Instanz beschaffen

Anwendungen interagieren über eine Instanz der EntityManager-Klasse mit JPA. Sie erhalten diese Instanz durch Instanziieren und Aufrufen einer Methode für eine Instanz der EntityManagerFactory-Klasse. Die Factory-Instanz verwendet die JPA-Konfiguration (gekennzeichnet durch den Namen ""transactions-optional""), um EntityManager-Instanzen zu erstellen.

Da die Initialisierung einer EntityManagerFactory-Instanz einige Zeit in Anspruch nimmt, sollten Sie eine einzelne Instanz so oft wie möglich wiederverwenden. Eine einfache Methode hierfür besteht darin, wie folgt eine Singleton-Wrapper-Klasse mit einer statischen Instanz zu erstellen:

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

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

Die Anwendung verwendet die Factory-Instanz zum Erstellen einer einzigen EntityManager-Instanz für jede Anfrage, die auf den Datenspeicher zugreift.

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

import EMF;

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

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

Wenn Sie mit der EntityManager-Instanz fertig sind, müssen Sie die zugehörige close()-Methode aufrufen. Es ist ein Fehler, die EntityManager-Instanz nach dem Aufrufen ihrer close()-Methode zu verwenden.

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

Klassen- und Feldannotationen

Jedes von JPA gespeicherte Objekt wird zu einer Entität im App Engine-Datenspeicher. Der Typ der Entität wird aus dem einfachen Namen der Klasse abgeleitet (ohne Paketnamen). Jedes persistente Feld der Klasse stellt eine Property der Entität dar. Dabei ist der Name der Property gleich dem Namen des Felds (unter Beibehaltung der Groß- und Kleinschreibung).

Wenn Sie eine Java-Klasse so deklarieren möchten, dass sie mit JPA im Datenspeicher gespeichert und daraus abgerufen werden kann, vergeben Sie für die Klasse eine @Entity-Annotation. Beispiel:

import javax.persistence.Entity;

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

Felder der Datenklasse, die im Datenspeicher gespeichert werden sollen, müssen entweder einen Typ haben, der standardmäßig persistiert wird, oder explizit als persistent deklariert werden. Auf der DataNucleus-Website finden Sie eine Übersicht über das Standardverhalten der JPA-Persistenz. Wenn Sie ein Feld explizit als persistent deklarieren möchten, vergeben Sie dafür eine @Basic-Annotation:

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

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

// ...
    @Basic
    private ShortBlob data;

Felder können folgende Typen haben:

  • Einen der vom Datenspeicher unterstützten Typen
  • Eine Sammlung (z. B. ein java.util.List<...>) von Werten eines zentralen Datenspeichertyps
  • Eine Instanz oder Sammlung von Instanzen einer @Entity-Klasse
  • Eine eingebettete Klasse, gespeichert als Attribute der Entität

Datenklassen müssen einen öffentlichen oder geschützten Standardkonstruktor aufweisen und es muss ein Feld speziell für das Speichern des Primärschlüssels der entsprechenden Datenspeicher-Entität bestimmt sein. Sie können zwischen vier verschiedenen Arten von Schlüsselfeldern wählen, die jeweils einen anderen Werttyp und andere Annotationen verwenden. Weitere Informationen finden Sie unter Daten erstellen: Schlüssel. Das einfachste Schlüsselfeld weist als Wert eine lange Ganzzahl auf, die beim erstmaligen Speichern des Objekts im Datenspeicher automatisch von JPA eingetragen wird. Dieser Wert ist über alle Instanzen der Klasse hinweg eindeutig. Bei Schlüsseln in Form von Ganzzahlen vom Typ "long" werden die Annotationen @Idund @GeneratedValue(strategy = GenerationType.IDENTITY) verwendet:

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;

Hier sehen Sie ein Beispiel für eine Datenklasse:

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

Vererbung

JPA unterstützt das Erstellen von Datenklassen, die Vererbung verwenden. Bevor Sie sich damit befassen, wie in App Engine die JPA-Vererbung funktioniert, sollten Sie die DataNucleus-Dokumentation zu diesem Thema lesen und dann an dieser Stelle fortfahren. Fertig? OK Die JPA-Vererbung bei App Engine funktioniert so, wie in der DataNucleus-Dokumentation beschrieben. Allerdings gelten einige zusätzliche Einschränkungen. Wir erörtern diese Einschränkungen und geben dann einige konkrete Beispiele.

Mit der Vererbungsstrategie "JOINED" können Sie die Daten für ein einzelnes Datenobjekt auf mehrere "Tabellen" aufteilen. Da der App Engine-Datenspeicher allerdings keine Joins unterstützt, muss bei dieser Vererbungsstrategie für jede Vererbungsebene eines Datenobjekts ein Remoteprozeduraufruf ausgeführt werden. Da dies potenziell sehr ineffizient ist, wird die Vererbungsstrategie "JOINED" für Datenklassen nicht unterstützt.

Zweitens ermöglicht die Vererbungsstrategie "SINGLE_TABLE" das Speichern der Daten für ein Datenobjekt in einer einzelnen "Tabelle", die der persistenten Klasse im Stammverzeichnis Ihrer Vererbungshierarchie zugeordnet ist. Auch wenn diese Strategie keine grundsätzlichen Leistungsschwächen aufweist, wird sie dennoch derzeit nicht unterstützt. Dies wird sich möglicherweise in zukünftigen Versionen ändern.

Jetzt die gute Nachricht: Die Strategien "TABLE_PER_CLASS" und "MAPPED_SUPERCLASS" funktionieren wie in der DataNucleus-Dokumentation beschrieben. Ein Beispiel:

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

In diesem Beispiel haben wir die @MappedSuperclass-Annotation zur Deklaration der Klasse Worker hinzugefügt. Damit wird JPA angewiesen, alle persistenten Felder von Worker in den Datenspeicher-Entitäten seiner Unterklassen zu speichern. Die Datenspeicher-Entität, die durch den Aufruf von persist() mit einer Employee-Instanz erstellt wurde, hat zwei Attribute mit den Namen "department" und "salary". Die Datenspeicher-Entität, die durch den Aufruf von persist() mit einer Intern-Instanz erstellt wurde, hat zwei Attribute mit den Namen "department" und "internshipEndDate". Der Datenspeicher enthält keine Entitäten vom Typ "Worker".

Nun wollen wir die Sache ein wenig interessanter machen. Angenommen, wir wünschen neben Employee und Intern noch eine Spezialisierung von Employee für Arbeitnehmer, die das Unternehmen verlassen haben:

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

In diesem Beispiel haben wir die @Inheritance-Annotation zur Deklaration der Klasse FormerEmployee hinzugefügt, wobei das strategy-Attribut auf InheritanceType.TABLE_PER_CLASS gesetzt wurde. Damit wird JPA angewiesen, alle persistenten Felder von FormerEmployee und der zugehörigen übergeordneten Klassen in Datenspeicherentitäten zu speichern, die FormerEmployee-Instanzen entsprechen. Die Datastore-Entität, die als Ergebnis des Aufrufs von persist() mit einer FormerEmployee-Instanz erstellt wird, hat drei Attribute namens "department", "salary" und "lastDay". Es wird keine Entität vom Typ "Employee" geben, die einem FormerEmployee entspricht. Wenn Sie jedoch persist() mit einem Objekt aufrufen, dessen Laufzeittyp Employee ist, erstellen Sie damit eine Entität vom Typ "Employee".

Die Mischung von Beziehungen und Vererbung funktioniert so lange, wie die deklarierten Typen Ihrer Beziehungsfelder mit den Laufzeittypen der Objekte übereinstimmen, die Sie diesen Feldern zuweisen. Weitere Informationen finden Sie im Abschnitt über polymorphe Beziehungen. Dieser Abschnitt enthält JDO-Beispiele, die Konzepte und Einschränkungen gelten jedoch gleichermaßen für JPA.

Nicht unterstützte Funktionen von JPA 1.0

Die folgenden Funktionen der JPA-Schnittstelle werden nicht von der App Engine-Implementierung unterstützt:

  • Beanspruchte ("owned") m:n-Beziehungen und unbeanspruchte ("unowned") Beziehungen. Sie können unbeanspruchte Beziehungen mit expliziten Schlüsselwerten implementieren, auch wenn Typprüfungen nicht im API erzwungen werden.
  • "Join"-Abfragen. Bei der Durchführung einer Abfrage für einen übergeordneten Typ können Sie nicht das Feld einer untergeordneten Entität in einem Filter verwenden. Beachten Sie, dass Sie das Beziehungsfeld des übergeordneten Elements mithilfe eines Schlüssels direkt in einer Abfrage testen können.
  • Aggregationsabfragen (group by, having, sum, avg, max, min)
  • 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.