Datenklassen mit JDO definieren

Sie können JDO verwenden, um einfache Java-Datenobjekte (manchmal als "Plain Old Java Objects" oder "POJOs" bezeichnet) im Datenspeicher zu speichern. Jedes Objekt, das mit dem PersistenceManager persistent gemacht wird, wird zu einer Entität im Datenspeicher. Mit Annotationen wird JDO angewiesen, wie Instanzen der Datenklassen gespeichert und neu erstellt werden sollen.

Hinweis: Bei früheren Versionen von JDO werden statt Java-Annotationen .jdo-XML-Dateien verwendet. Diese funktionieren weiterhin mit JDO 2.3. Diese Dokumentation behandelt nur Java-Anmerkungen mit Datenklassen.

Klassen- und Feldannotationen

Jedes von JDO gespeicherte Objekt wird zu einer Entität im App Engine-Datenspeicher. Die Art der Entität wird aus dem einfachen Namen der Klasse abgeleitet. Bei inneren Klassen wird der $-Pfad ohne Paketnamen verwendet. Jedes persistente Feld der Klasse stellt eine Eigenschaft der Entität dar. Dabei ist der Name der Eigenschaft gleich dem Namen des Felds, unter Beibehaltung der Groß- und Kleinschreibung.

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

import javax.jdo.annotations.PersistenceCapable;

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

Felder der Datenklasse, die im Datenspeicher gespeichert werden sollen, müssen als persistente Felder deklariert werden. Um ein Feld als persistent zu deklarieren, versehen Sie es mit einer @Persistent-Annotation:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Um ein Feld als nicht persistent zu deklarieren, sodass es nicht im Datenspeicher gespeichert und beim Abrufen des Objekts nicht wiederhergestellt wird, versehen Sie es mit einer @NotPersistent-Annotation.

Tipp: Laut JDO-Festlegung sind die Felder eines bestimmten Typs standardmäßig persistent, wenn weder die Annotation @Persistent noch die Annotation @NotPersistent angegeben wird. Die Felder aller anderen Typen sind standardmäßig nicht persistent. Eine vollständige Beschreibung dieses Verhaltens finden Sie in der DataNucleus-Dokumentation. Da nicht alle zentralen Werttypen des App Engine-Datenspeichers standardmäßig gemäß der JDO-Spezifikation persistent sind, empfehlen wir, Felder explizit mit @Persistent- oder @NotPersistent-Annotationen zu versehen, um es klarzustellen.

Felder können folgende Typen aufweisen. Diese werden im Folgenden detailliert beschrieben.

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

Bei einer Datenklasse muss ein einzelnes Feld speziell für das Speichern des Primärschlüssels der entsprechenden Datenspeicherentitä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 flexibelste Schlüsselfeld ist ein Key-Objekt, das beim erstmaligen Speichern des Objekts im Datenspeicher automatisch einen Wert von JDO erhält, der über alle Instanzen der Klassen hinweg einmalig ist. Für Primärschlüssel vom Typ Key sind eine @PrimaryKey-Annotation und eine @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)-Annotation erforderlich:

Tipp: Setzen Sie alle Ihre persistenten Felder auf private oder protected oder versehen Sie sie mit Paketschutz, und lassen Sie öffentlichen Zugriff nur über Accessor-Methoden zu. Durch den direkten Zugriff auf ein persistentes Feld von einer anderen Klasse aus wird eventuell das JDO-Klassen-Enhancement umgangen. Alternativ können Sie andere Klassen als @PersistenceAware festlegen. Weitere Informationen finden Sie in der DataNucleus-Dokumentation.

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

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PrimaryKey;

// ...
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.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.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String firstName;

    @Persistent
    private String lastName;

    @Persistent
    private Date hireDate;

    public Employee(String firstName, String lastName, Date hireDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.hireDate = hireDate;
    }

    // Accessors for the fields. JDO 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;
    }
}

Zentrale Werttypen

Zur Darstellung einer Eigenschaft, die einen einzelnen Wert eines zentralen Typs enthält, deklarieren Sie ein Feld mit Java-Typ und verwenden Sie die Annotation @Persistent:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Serialisierbare Objekte

Ein Feldwert kann eine Instanz einer serialisierbaren Klasse ("Serializable") enthalten, in der der serialisierte Wert einer Instanz in einem einzigen Eigenschaftswert des Typs Blob gespeichert wird. Das Feld verwendet die Annotation @Persistent(serialized=true), um die JDO zur Serialisierung des Werts anzuweisen. Blob-Werte werden nicht indexiert und können nicht bei Abfragefiltern oder Sortierreihenfolgen verwendet werden.

Hier sehen Sie ein Beispiel für eine einfache serialisierbare Klasse, die eine Datei darstellt, einschließlich Dateiinhalten, Dateiname und MIME-Typ. Dies ist keine JDO-Datenklasse, weshalb es keine Persistenzannotationen gibt.

import java.io.Serializable;

public class DownloadableFile implements Serializable {
    private byte[] content;
    private String filename;
    private String mimeType;

    // ... accessors ...
}

So können Sie eine Instanz einer serialisierbaren Klasse als Blob-Wert in einer Eigenschaft speichern, ein Feld deklarieren, dessen Typ die Klasse ist, und die Annotation @Persistent(serialized = "true") verwenden:

import javax.jdo.annotations.Persistent;
import DownloadableFile;

// ...
    @Persistent(serialized = "true")
    private DownloadableFile file;

Untergeordnete Objekte und Beziehungen

Ein Feldwert, bei dem es sich um eine Instanz einer @PersistenceCapable-Klasse handelt, erstellt eine beanspruchte ("owned") 1-zu-1-Beziehung zwischen zwei Objekten. Ein Feld, bei dem es sich um eine Sammlung derartiger Referenzen handelt, erstellt eine beanspruchte 1-zu-N-Beziehung.

Wichtig: Eigene Beziehungen haben Auswirkungen auf Transaktionen, Entitätsgruppen und kaskadierende Löschvorgänge. Weitere Informationen finden Sie unter Transaktionen und Beziehungen.

Hier sehen Sie ein einfaches Beispiel für eine beanspruchte 1-zu-1-Beziehung zwischen einem Employee-Objekt und einem ContactInfo-Objekt:

ContactInfo.java

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

@PersistenceCapable
public class ContactInfo {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String streetAddress;

    @Persistent
    private String city;

    @Persistent
    private String stateOrProvince;

    @Persistent
    private String zipCode;

    // ... accessors ...
}

Employee.java

import ContactInfo;
// ... imports ...

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private ContactInfo myContactInfo;

    // ... accessors ...
}

Wenn die App in diesem Beispiel eine Employee-Instanz erstellt, ihr myContactInfo-Feld mit einer neuen ContactInfo-Instanz ausfüllt und anschließend die Employee-Instanz mit pm.makePersistent(...) speichert, erstellt der Datenspeicher zwei Entitäten. Eine weist die Art "ContactInfo" auf und steht für die ContactInfo-Instanz. Die andere weist die Art "Employee" auf. Für den Schlüssel der ContactInfo-Entität gilt der Schlüssel der Employee-Entität als übergeordnetes Element in der Entitätsgruppe.

Eingebettete Klassen

Mit eingebetteten Klassen ("Embedded") können Sie einen Feldwert mithilfe einer Klasse modellieren, ohne eine neue Datenspeicherentität zu erstellen und eine Beziehung herzustellen. Die Felder des Objektwerts sind direkt in der Datenspeicherentität für das beinhaltende Objekt gespeichert.

Jede beliebige @PersistenceCapable-Datenklasse kann als eingebettetes Objekt in einer anderen Datenklasse verwendet werden. Die @Persistent-Felder der Klasse sind im Objekt eingebettet. Wenn Sie die einzubettende Klasse mit der Annotation @EmbeddedOnly versehen, kann die Klasse nur als eingebettete Klasse verwendet werden. Für die eingebettete Klasse ist kein Primärschlüsselfeld erforderlich, da es nicht als separate Entität gespeichert wird.

Hier sehen Sie ein Beispiel für eine eingebettete Klasse. Dieses Beispiel macht die eingebettete Klasse zu einer inneren Klasse der Datenklasse, die sie verwendet. Dies ist nützlich, aber nicht notwendig, um eine Klasse einbettbar zu machen.

import javax.jdo.annotations.Embedded;
import javax.jdo.annotations.EmbeddedOnly;
// ... imports ...

@PersistenceCapable
public class EmployeeContacts {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    Key key;
    @PersistenceCapable
    @EmbeddedOnly
    public static class ContactInfo {
        @Persistent
        private String streetAddress;

        @Persistent
        private String city;

        @Persistent
        private String stateOrProvince;

        @Persistent
        private String zipCode;

        // ... accessors ...
    }

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;
}

Die Felder einer eingebetteten Klasse werden als Eigenschaften in der Entität gespeichert. Dabei werden der Name jedes Felds und der Name der zugehörigen Eigenschaft verwendet. Wenn das Objekt mehrere Felder mit einer eingebetteten Klasse als Typ aufweist, müssen Sie die Felder umbenennen, sodass keine Konflikte zwischen ihnen auftreten. Neue Feldnamen werden mithilfe von Argumenten für die @Embedded-Annotation angegeben. Beispiel:

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;

    @Persistent
    @Embedded(members = {
        @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")),
        @Persistent(name="city", columns=@Column(name="workCity")),
        @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")),
        @Persistent(name="zipCode", columns=@Column(name="workZipCode")),
    })
    private ContactInfo workContactInfo;

Ebenso dürfen Felder im Objekt keine Namen verwenden, die zu Konflikten mit Feldern eingebetteter Klassen führen, es sei denn, die eingebetteten Felder werden umbenannt.

Da die persistenten Eigenschaften der eingebetteten Klasse in derselben Entität gespeichert werden wie die anderen Felder, können Sie persistente Felder der eingebetteten Klasse in JDOQL-Abfragefiltern und Sortierreihenfolgen verwenden. Auf das eingebettete Feld kann über den Namen des äußeren Felds, einen Punkt (.) und den Namen des eingebetteten Felds verwiesen werden. Dies funktioniert unabhängig davon, ob die Eigenschaftsnamen für die eingebetteten Felder mithilfe von @Column-Annotationen geändert wurden oder nicht.

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

Sammlungen

Eine Datenspeichereigenschaft kann mehrere Werte aufweisen. In JDO wird dies durch ein einzelnes Feld mit einem Sammlungstyp dargestellt, wobei die Sammlung aus einem der zentralen Werttypen oder einer Serializable-Klasse besteht. Die folgenden Sammlungstypen werden unterstützt:

  • java.util.ArrayList<...>
  • java.util.HashSet<...>
  • java.util.LinkedHashSet<...>
  • java.util.LinkedList<...>
  • java.util.List<...>
  • java.util.Map<...>
  • java.util.Set<...>
  • java.util.SortedSet<...>
  • java.util.Stack<...>
  • java.util.TreeSet<...>
  • java.util.Vector<...>

Wenn ein Feld als Liste deklariert wird, weisen vom Datenspeicher zurückgegebene Objekte einen ArrayList-Wert auf. Wenn ein Feld als Set deklariert wird, gibt der Datenspeicher ein HashSet zurück. Wenn ein Feld als SortedSet deklariert wird, gibt der Datenspeicher ein TreeSet zurück.

So wird beispielsweise ein Feld, dessen Typ List<String> ist, als null oder mehr Stringwerte für die Eigenschaft gespeichert, einer für jeden Wert in der List.

import java.util.List;
// ... imports ...

// ...
    @Persistent
    List<String> favoriteFoods;

Eine Sammlung von untergeordneten Objekten (von @PersistenceCapable-Klassen) erstellt mehrere Entitäten mit einer 1-zu-N-Beziehung. Siehe Beziehungen.

Datenspeichereigenschaften mit mehreren Werten weisen ein spezielles Verhalten für Abfragefilter und Sortierreihenfolgen auf. Weitere Informationen finden Sie auf der Seite Datenspeicherabfragen.

Objektfelder und Entitätseigenschaften

Der App Engine-Datenspeicher unterscheidet zwischen Entitäten ohne angegebene Eigenschaft und Entitäten mit einem null-Wert für eine Eigenschaft. JDO unterstützt diese Unterscheidung nicht: Jedes Feld eines Objekts weist einen Wert auf, möglicherweise null. Wenn ein Feld mit einem Werttyp, der NULL-Werte zulässt (andere als die integrierten Typen, wie int oder boolean), beim Speichern des Objekts auf null gesetzt wird, wird für die entstehende Entität die Eigenschaft mit einem NULL-Wert festgelegt.

Wenn eine Datenspeicherentität in ein Objekt geladen wird und keine Eigenschaft für eines der Felder des Objekts aufweist und der Typ des Felds ein einwertiger Typ ist, der NULL-Werte zulässt, wird das Feld auf null gesetzt. Wenn das Objekt wieder im Datenspeicher gespeichert wird, wird die null-Eigenschaft im Datenspeicher auf den NULL-Wert gesetzt. Wenn das Feld einen Werttyp aufweist, der keine Nullwerte zulässt, wird beim Laden einer Entität ohne die entsprechende Eigenschaft eine Ausnahme ausgelöst. Dies passiert nicht, wenn die Entität aus derselben JDO-Klasse erstellt wurde, die auch zur erneuten Erstellung der Instanz verwendet wird; kann jedoch passieren, wenn sich die JDO-Klasse ändert oder wenn die Entität mit der untergeordneten API anstelle von JDO erstellt wurde.

Wenn es sich beim Typ eines Felds um eine Sammlung eines zentralen Datentyps oder eine serialisierbare Klasse handelt und es keine Werte für die Eigenschaft in der Entität gibt, wird die leere Sammlung im Datenspeicher dargestellt, indem die Eigenschaft auf einen einzelnen Nullwert gesetzt wird. Wenn es sich bei dem Typ des Felds um einen Arraytyp handelt, wird ihm ein Array von 0 Elementen zugewiesen. Wenn das Objekt geladen wird und es keinen Wert für die Eigenschaft gibt, wird dem Feld eine leere Sammlung des entsprechenden Typs zugewiesen. Intern kennt der Datenspeicher den Unterschied zwischen einer leeren Sammlung und einer Sammlung, die einen einzelnen Nullwert enthält.

Wenn die Entität eine Eigenschaft ohne ein entsprechendes Feld im Objekt aufweist, kann nicht über das Objekt auf die betreffende Eigenschaft zugegriffen werden. Wenn das Objekt wieder im Datenspeicher gespeichert wird, wird die zusätzliche Eigenschaft gelöscht.

Wenn eine Entität eine Eigenschaft aufweist, deren Wert von dem zugehörigen Feld im Objekt abweicht, versucht JDO, den Wert in einem Cast-Vorgang in den Feldtyp umzuwandeln. Wenn der Wert nicht in den Feldtyp umgewandelt werden kann, löst JDO eine ClassCastException aus. Bei Zahlen (ganze Zahlen vom Typ "long" und Gleitkommazahlen doppelter Breite) wird der Wert konvertiert und nicht in einem Cast-Vorgang umgewandelt. Wenn der Wert der numerischen Eigenschaft größer ist als der Feldtyp, tritt bei der Konvertierung ein Überlauf auf, ohne dass eine Ausnahme ausgelöst wird.

Sie können eine Eigenschaft als nicht indexiert deklarieren, indem Sie die folgende Zeile

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

über der Eigenschaft in der Klassendefinition hinzufügen. Weitere Informationen darüber, was es bedeutet, wenn eine Eigenschaft nicht indexiert ist, finden Sie im Abschnitt Nicht indexierte Eigenschaften der Hauptdokumentation.

Übernahme

Es ist ganz natürlich, Datenklassen zu erstellen, die Vererbung nutzen, und JDO unterstützt dies auch. Bevor wir darüber sprechen, wie die JDO-Vererbung in App Engine funktioniert, empfehlen wir Ihnen, die DataNucleus-Dokumentation zu diesem Thema zu lesen und dann zurückzukommen. Erledigt? OK. Die JDO-Vererbung bei App Engine funktioniert so, wie in der DataNucleus-Dokumentation beschrieben. Allerdings gelten einige zusätzliche Einschränkungen. Wir besprechen diese Einschränkungen und geben dann einige konkrete Beispiele an.

Mit der Vererbungsstrategie "new-table" können Sie die Daten für ein einzelnes Datenobjekt auf mehrere "Tabellen" aufteilen. Da der App Engine-Datenspeicher aber keine Joins unterstützt, erfordert die Arbeit an einem Datenobjekt mit dieser Vererbungsstrategie einen Remoteprozeduraufruf für jede Vererbungsebene. Dies ist möglicherweise sehr ineffizient, daher wird die Vererbungsstrategie "new-table" nicht für Datenklassen unterstützt, die nicht die Wurzel ihrer Vererbungshierarchien sind.

Zweitens erlaubt die Vererbungsstrategie "superclass-table", die Daten für ein Datenobjekt in der "Tabelle" ihrer Oberklasse zu speichern. Auch wenn diese Strategie keine grundsätzlichen Leistungsschwächen aufweist, so wird sie dennoch derzeit nicht unterstützt. Dies wird sich möglicherweise in zukünftigen Versionen ändern.

Nun die gute Nachricht: Die Strategien "subclass-table" und "complete-table" funktionieren wie in der DataNucleus-Dokumentation beschrieben und Sie können "new-table" auch für jedes Datenobjekt verwenden, das die Wurzel der Vererbungshierarchie bildet. Sehen wir uns ein Beispiel an:

Worker.java

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public abstract class Worker {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String department;
}

Employee.java

// ... imports ...

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

Intern.java

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

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

In diesem Beispiel haben wir die @Inheritance-Annotation zur Deklaration der Klasse Worker hinzugefügt, wobei das strategy>-Attribut auf InheritanceStrategy.SUBCLASS_TABLE gesetzt wurde. Damit wird JDO angewiesen, alle persistenten Felder von Worker in den Datenspeicherentitäten der zugehörigen Unterklassen zu speichern. Die Datenspeicherentität, die durch den Aufruf von makePersistent() mit einer Employee-Instanz erstellt wurde, hat zwei Eigenschaften mit den Namen "department" und "salary". Die Datenspeicherentität, die durch den Aufruf von makePersistent() mit einer Intern-Instanz erstellt wurde, hat zwei Eigenschaften 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;
// ... imports ...

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

In diesem Beispiel haben wir die @Inheritance-Annotation zur Deklaration der Klasse FormerEmployee hinzugefügt, wobei das custom-strategy>-Attribut auf "complete-table" gesetzt wurde. Damit wird JDO 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 makePersistent() mit einer FormerEmployee-Instanz erstellt wird, hat drei Eigenschaften namens "department", "salary" und "lastDay". Keine Entität der Art "Employee" entspricht einem FormerEmployee. Wenn Sie jedoch makePersistent() mit einem Objekt mit dem Laufzeittyp Employee aufrufen, erstellen Sie eine Entität der Art "Employee".

Die Mischung von Beziehungen mit 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.