Datastore-Abfragen in JDO

In diesem Dokument wird die Verwendung des Persistenzframeworks Java Data Objects (JDO) für App Engine-Datenspeicherabfragen behandelt. Allgemeine Informationen zu Abfragen finden Sie auf der Seite Datastore-Abfragen.

Bei einer Abfrage werden Entitäten aus Datastore abgerufen, die eine Reihe bestimmter Bedingungen erfüllen. Diese Abfrage wird für Entitäten einer bestimmten Art ausgeführt. Sie kann Filter für die Attributwerte, Schlüssel und Ancestors der Entitäten enthalten und null oder mehr Entitäten als Ergebnisse zurückgeben. Außerdem können mit einer Abfrage Sortierfolgen festgelegt werden, um die Ergebnisse nach ihren Attributwerten zu sortieren. Die Ergebnisse umfassen alle Entitäten mit mindestens einem Wert (ggf. auch null) für jedes Attribut, das in den Filtern und Sortierfolgen angegeben wird, und deren Attributwerte alle angegebenen Filterkriterien erfüllen. Die Abfrage kann ganze Entitäten, projizierte Entitäten oder einfach Schlüssel von Entitäten zurückgeben.

Eine typische Abfrage umfasst

  • Eine Entitätsart, auf die die Abfrage angewendet wird
  • Null oder mehr Filter, die auf Attributwerten, Schlüsseln und Ancestors der Entitäten basieren
  • Null oder mehr Sortierfolgen zum Anordnen der Ergebnisse
Bei der Ausführung ruft die Abfrage alle Entitäten der angegebenen Art ab, die allen in der angegebenen Reihenfolge sortierten Filtern entsprechen. Abfragen werden schreibgeschützt ausgeführt.

Hinweis: Um Arbeitsspeicher zu sparen und die Leistung zu verbessern, sollte eine Abfrage, wenn möglich, einen Grenzwert für die Anzahl von zurückgegebenen Ergebnissen angeben.

Hinweis: Der indexbasierte Abfragemechanismus unterstützt ein breites Spektrum an Abfragen und ist für die meisten Anwendungen geeignet. Allerdings werden einige Arten von Abfragen, die in anderen Datenbanktechnologien üblich sind, vom Abfragemodul in Datastore nicht unterstützt, insbesondere Join- und Aggregatabfragen. Weitere Informationen zu Einschränkungen bei Datastore-Abfragen finden Sie auf der Seite Datastore-Abfragen.

Abfragen mit JDOQL

JDO umfasst eine Abfragesprache für das Abrufen von Objekten, die bestimmte Kriterien erfüllen. Diese als JDOQL bezeichnete Sprache bezieht sich direkt auf JDO-Datenklassen und -felder und beinhaltet Typprüfungen für Abfrageparameter und -ergebnisse. JDOQL hat Ähnlichkeit mit SQL, eignet sich jedoch besser für objektorientierte Datenbanken wie App Engine Datastore. (Die App Engine-Implementierung der JDO API unterstützt keine direkten SQL-Abfragen.)

Die Query-Schnittstelle von JDO unterstützt mehrere Aufrufarten: Mit der JDOQL-Stringsyntax können Sie eine vollständige Abfrage in Form eines Strings oder einige bzw. alle Teile der Abfrage durch Aufrufen von Methoden für das Objekt Query angeben. Im folgenden Beispiel wird das Aufrufen über Methoden mit einem Filter und einer Sortierfolge unter Verwendung der Parameterersetzung für den im Filter verwendeten Wert gezeigt. Die Argumentwerte, die an die Methode execute() des Objekts Query übergeben werden, werden in der Abfrage in der angegebenen Reihenfolge ersetzt:

import java.util.List;
import javax.jdo.Query;

// ...

Query q = pm.newQuery(Person.class);
q.setFilter("lastName == lastNameParam");
q.setOrdering("height desc");
q.declareParameters("String lastNameParam");

try {
  List<Person> results = (List<Person>) q.execute("Smith");
  if (!results.isEmpty()) {
    for (Person p : results) {
      // Process result p
    }
  } else {
    // Handle "no results" case
  }
} finally {
  q.closeAll();
}

Hier ist die gleiche Abfrage unter Verwendung der String-Syntax:

Query q = pm.newQuery("select from Person " +
                      "where lastName == lastNameParam " +
                      "parameters String lastNameParam " +
                      "order by height desc");

List<Person> results = (List<Person>) q.execute("Smith");

Sie können diese Stile zur Abfragendefinition mischen. Beispiel:

Query q = pm.newQuery(Person.class,
                      "lastName == lastNameParam order by height desc");
q.declareParameters("String lastNameParam");

List<Person> results = (List<Person>) q.execute("Smith");

Sie können eine einzelne Query-Instanz mit unterschiedlichen für die Parameter ersetzten Werten wiederverwenden, indem Sie die Methode execute() mehrmals aufrufen. Bei jedem Aufruf wird die Abfrage durchgeführt und die Ergebnisse als Sammlung zurückgegeben.

Die JDOQL-Stringsyntax unterstützt die Literalspezifikation von String- und numerischen Werten. Für alle anderen Werttypen muss die Parameterersetzung verwendet werden. Literale innerhalb des Abfragestrings können entweder in einfache (') oder doppelte (") Anführungszeichen gesetzt werden. In folgendem Beispiel wird ein String-Literal verwendet:

Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' order by height desc");

Filter

In einem Attributfilter wird Folgendes angegeben:

  • Ein Attributname
  • Ein Vergleichsoperator
  • Ein Attributwert
Beispiel:

Filter propertyFilter =
    new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
Query q = new Query("Person").setFilter(propertyFilter);
Query q = pm.newQuery(Person.class);
q.setFilter("height <= maxHeight");

Der Attributwert muss von der Anwendung angegeben werden; er kann keine anderen Attribute referenzieren oder anhand anderer Attribute berechnet werden. Eine Entität erfüllt den Filter, wenn sie eine Property mit dem angegebenen Namen enthält, deren Wert mit dem in dem Filter angegebenen Wert in der vom Vergleichsoperator beschriebenen Weise verglichen wird.

Einer der folgenden Vergleichsoperatoren kann verwendet werden:

Operator Bedeutung
== Gleich
< Kleiner als
<= Kleiner als oder gleich
> Größer als
>= Größer als oder gleich
!= Ungleich

Wie auf der Hauptseite zu Abfragen beschrieben, können in einer einzelnen Abfrage keine Ungleichheitsfilter (<, <=, >, >=, !=) auf mehr als einem Attribut verwendet werden. Mehrere Ungleichheitsfilter für dieselbe Eigenschaft (z. B. Abfrage für einen Wertebereich) sind zulässig. contains()-Filter, die IN-Filtern in SQL entsprechen, werden mit der folgenden Syntax unterstützt:

// Query for all persons with lastName equal to Smith or Jones
Query q = pm.newQuery(Person.class, ":p.contains(lastName)");
q.execute(Arrays.asList("Smith", "Jones"));

Mit dem Operator "nicht gleich" (!=) werden tatsächlich zwei Abfragen ausgeführt: eine, bei der alle anderen Filter unverändert sind und der Ungleichheitsfilter durch den Kleiner-als-Filter (<) ersetzt wird, und eine, bei der er durch den Größer-als-Filter (>) ersetzt wird. Danach werden die Ergebnisse in der entsprechenden Reihenfolge zusammengeführt. Eine Abfrage darf nur einen Not-equal-Filter enthalten, und wenn das der Fall ist, darf sie keine anderen Ungleichheitsfilter enthalten.

Mit dem Operator contains() werden ebenfalls mehrere Abfragen ausgeführt: Eine für jedes Element in der angegebenen Liste, wobei alle anderen Filter unverändert bleiben und der Filter contains() durch einen Gleichheitsfilter (==) ersetzt wird. Die Ergebnisse werden in der Reihenfolge der Elemente in der Liste zusammengeführt. Wenn eine Abfrage mehr als einen contains()-Filter enthält, werden mehrere Abfragen ausgeführt, und zwar jeweils eine für jede mögliche Kombination von Werten in den contains()-Listen.

Eine einzelne Abfrage mit den Operatoren „nicht gleich” (!=) oder contains() ist auf maximal 30 Unterabfragen begrenzt.

Weitere Informationen darüber, wie Abfragen mit != und contains() in einem JDO/JPA-Framework in mehrere Abfragen umgesetzt werden, finden Sie im Artikel Abfragen mit den Filtern != und IN.

In der JDOQL-Stringsyntax können Sie mehrere Filter mit den Operatoren && (logisches "und") und || trennen (logisches "oder"):

q.setFilter("lastName == 'Smith' && height < maxHeight");

Negation (logisches "nicht") wird nicht unterstützt. Beachten Sie auch, dass der Operator || nur verwendet werden kann, wenn die Filter, die damit getrennt werden, denselben Attributnamen haben (d. h., wenn sie zu einem einzigen contains()-Filter kombiniert werden können):

// Legal: all filters separated by || are on the same property
Query q = pm.newQuery(Person.class,
                      "(lastName == 'Smith' || lastName == 'Jones')" +
                      " && firstName == 'Harold'");

// Not legal: filters separated by || are on different properties
Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' || firstName == 'Harold'");

Sortierfolgen

Eine Abfrage-Sortierfolge gibt Folgendes an:

  • Ein Attributname
  • Eine Sortierfolge (aufsteigend oder absteigend)

Beispiel:

// Order alphabetically by last name:
Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING);

// Order by height, tallest to shortest:
Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING);

Beispiel:

// Order alphabetically by last name:
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc");

// Order by height, tallest to shortest:
Query q = pm.newQuery(Person.class);
q.setOrdering("height desc");

Wenn eine Abfrage mehrere Sortierfolgen enthält, werden diese in der angegebenen Abfolge angewendet. Im folgenden Beispiel wird zuerst aufsteigend nach Nachname und dann absteigend nach Körpergröße sortiert:

Query q =
    new Query("Person")
        .addSort("lastName", SortDirection.ASCENDING)
        .addSort("height", SortDirection.DESCENDING);
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc, height desc");

Wenn keine Sortierfolgen angegeben sind, werden die Ergebnisse in der Reihenfolge zurückgegeben, in der sie aus Datastore abgerufen werden.

Hinweis: Aufgrund der Art, wie Datastore Abfragen ausführt, muss Folgendes beachtet werden: Wenn in einer Abfrage Ungleichheitsfilter für ein Attribut und Sortierfolgen für andere Attribute angegeben sind, muss das Attribut, das mit Ungleichheitsfiltern verwendet wird, vor den anderen Attributen sortiert werden.

Bereiche

In einer Abfrage kann ein Bereich für die Ergebnisse angegeben werden, die an die Anwendung zurückgeliefert werden sollen. Mit dem Bereich wird bestimmt, welche Ergebnisse der vollständigen Ergebnismenge als erste und letzte zurückgeliefert werden sollen. Die Ergebnisse werden anhand ihrer numerischen Indexe identifiziert, wobei 0 das erste Ergebnis in der Menge angibt. Zum Beispiel wird bei einem Bereich von 5, 10 das 6. bis 10. Ergebnis zurückgeliefert:

q.setRange(5, 10);

Hinweis: Die Verwendung von Bereichen kann sich auf die Leistung auswirken, da Datastore zuerst alle Ergebnisse abrufen und danach die dem gewünschten Bereich vorangehenden verwerfen muss. So werden z. B. bei einer Abfrage mit einem Bereich von 5, 10 zehn Ergebnisse aus dem Datastore abgerufen, die ersten fünf verworfen und die verbleibenden fünf an die Anwendung zurückgeliefert.

Schlüsselbasierte Abfragen

Entitätsschlüssel können Gegenstand eines Abfragefilters oder einer Sortierreihenfolge sein. Bei derartigen Abfragen wird von Datastore der vollständige Schlüsselwert berücksichtigt, einschließlich des Ancestor-Pfads der Entität, des Typs und des von der Anwendung zugewiesenen Schlüsselnamen-Strings bzw. der vom System zugewiesenen numerischen ID. Da der Schlüssel für alle Entitäten im System eindeutig ist, ermöglichen Schlüsselabfragen das einfache Abrufen von Entitäten eines bestimmten Typs in Stapeln, z. B. für einen Batch-Dump des Inhalts des Datastores. Anders als bei JDOQL-Bereichen kann dieser Vorgang für beliebig viele Entitäten effizient ausgeführt werden.

Bei einem Vergleich auf Ungleichheit werden die Schlüssel nach den folgenden Kriterien in folgender Reihenfolge angeordnet:

  1. Ancestor-Pfad
  2. Entitätstyp
  3. Kennzeichnung (Schlüsselname oder numerische ID)

Elemente des Ancestor-Pfads werden ähnlich verglichen: nach Art (String), dann nach Schlüsselname oder numerischer ID. Arten und Schlüsselnamen sind Strings und werden nach Bytewert sortiert; numerische IDs sind ganze Zahlen und werden numerisch sortiert. Wenn Entitäten mit derselben übergeordneten Entität und derselben Art eine Mischung aus Schlüsselnamen-Strings und numerischen IDs verwenden, stehen die Entitäten mit numerischen IDs vor den Entitäten mit Schlüsselnamen.

In JDO verweisen Sie mithilfe des Primärschlüsselfelds des Objekts auf den Entitätsschlüssel in der Abfrage. Wenn Sie einen Schlüssel als Abfragefilter verwenden möchten, geben Sie für die Methode declareParameters() den Parametertyp Key an. Mit der folgenden Abfrage werden alle Entitäten vom Typ Person mit einem bestimmten Lieblingsessen gefunden. Dabei wird vorausgesetzt, dass zwischen Person und Food eine unbeanspruchte ("unowned") 1-zu-1-Beziehung besteht:

Food chocolate = /*...*/;

Query q = pm.newQuery(Person.class);
q.setFilter("favoriteFood == favoriteFoodParam");
q.declareParameters(Key.class.getName() + " favoriteFoodParam");

List<Person> chocolateLovers = (List<Person>) q.execute(chocolate.getKey());

Mit einer ausschließlich schlüsselbasierten Abfrage werden nur die Schlüssel der Ergebnisentitäten anstelle der Entitäten selbst zurückgegeben. Dies führt zu niedrigerer Latenz und geringeren Kosten als beim Abruf ganzer Entitäten.

Query q = new Query("Person").setKeysOnly();
Query q = pm.newQuery("select id from " + Person.class.getName());
List<String> ids = (List<String>) q.execute();

Häufig ist es wirtschaftlicher, zuerst eine ausschließlich schlüsselbasierte Abfrage durchzuführen und dann einen Teil der Entitäten aus den Ergebnissen abzurufen als eine allgemeine Abfrage auszuführen, die unter Umständen mehr Entitäten abruft, als tatsächlich benötigt werden.

Erweiterungen (Extents)

Mit einer JDO-Erweiterung wird jedes Objekt einer bestimmten Klasse im Datenspeicher dargestellt. Übergeben Sie dazu die gewünschte Klasse an die Methode getExtent() des Persistence Managers. Mit der Schnittstelle Extent wird die Schnittstelle Iterable um den Zugriff auf Ergebnisse erweitert, die bei Bedarf in Batches abgerufen werden können. Wenn der Zugriff auf die Ergebnisse abgeschlossen ist, rufen Sie die Methode closeAll() der Erweiterung auf.

Im folgenden Beispiel erfolgt der Zugriff auf jedes Person-Objekt im Datenspeicher:

import java.util.Iterator;
import javax.jdo.Extent;

// ...

Extent<Person> extent = pm.getExtent(Person.class, false);
for (Person p : extent) {
  // ...
}
extent.closeAll();

Entitäten durch Abfrage löschen

Wenn Sie eine Abfrage mit dem Ziel ausführen, alle Entitäten zu löschen, die dem Abfragefilter entsprechen, können Sie sich mit der JDO-Funktion "delete by query" (Löschen durch Abfrage) Programmierarbeit sparen. Mit der folgenden Abfrage werden alle Personen ab einer bestimmten Körpergröße gelöscht:

Query q = pm.newQuery(Person.class);
q.setFilter("height > maxHeightParam");
q.declareParameters("int maxHeightParam");
q.deletePersistentAll(maxHeight);

Wie Sie sehen, besteht hier der einzige Unterschied darin, dass q.deletePersistentAll() anstelle von q.execute() aufgerufen wird. Alle oben für Filter, Sortierfolgen und Indexe beschriebenen Regeln und Einschränkungen gelten für Abfragen unabhängig davon, ob die Ergebnismenge ausgewählt oder gelöscht wird. Beachten Sie jedoch, dass genau wie beim Löschen dieser Entitäten vom Typ Person mit pm.deletePersistent() etwaige abhängige untergeordnete Elemente der durch die Abfrage gelöschten Entitäten ebenfalls gelöscht werden. Weitere Informationen zu abhängigen untergeordneten Elementen finden Sie auf der Seite Entitätsbeziehungen in JDO.

Abfragecursor

In JDO können mithilfe einer Erweiterung und der Klasse JDOCursorHelper mit JDO-Abfragen verwendet werden. Cursor kommen zum Einsatz, wenn Ergebnisse als Liste oder mithilfe eines Iterators abgerufen werden. Wenn Sie einen Cursor erhalten möchten, übergeben Sie entweder die Ergebnisliste oder den Iterator an die statische Methode JDOCursorHelper.getCursor():

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jdo.Query;
import com.google.appengine.api.datastore.Cursor;
import org.datanucleus.store.appengine.query.JDOCursorHelper;

Query q = pm.newQuery(Person.class);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the first 20 results

Cursor cursor = JDOCursorHelper.getCursor(results);
String cursorString = cursor.toWebSafeString();
// Store the cursorString

// ...

// Query q = the same query that produced the cursor
// String cursorString = the string from storage
Cursor cursor = Cursor.fromWebSafeString(cursorString);
Map<String, Object> extensionMap = new HashMap<String, Object>();
extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
q.setExtensions(extensionMap);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the next 20 results

Weitere Informationen zu Abfragecursorn finden Sie auf der Seite Datenspeicherabfragen.

Leserichtlinie und Zeitlimit für den Aufruf des Datenspeichers

Sie können die Leserichtlinie (Strong Consistency oder Eventual Consistency) und das Zeitlimit für Datenspeicheraufrufe für alle von einer PersistenceManager-Instanz vorgenommenen Aufrufe mithilfe der Konfiguration festlegen. Sie können diese Optionen auch für eine einzelne Query überschreiben. (Beachten Sie jedoch, dass es für diese Optionen keine Möglichkeit zum Überschreiben der Konfiguration gibt, wenn Sie Entitäten nach Schlüssel abrufen.)

Bei Auswahl von Eventual Consistency für eine Datenspeicherabfrage erfolgt der Zugriff auf die Indexe, die in der Abfrage für die Sammlung der Ergebnisse verwendet werden, ebenfalls mit Eventual Consistency. Gelegentlich werden dabei von Abfragen Entitäten zurückgeliefert, die nicht den Abfragekriterien entsprechen. Dies gilt jedoch auch für eine Leserichtlinie mit Strong Consistency. (Wenn in der Abfrage ein Ancestor-Filter verwendet wird, können Sie Transaktionen verwenden, um eine konsistente Ergebnismenge sicherzustellen.)

Wenn Sie die Leserichtlinie für eine einzelne Abfrage überschreiben möchten, rufen Sie die entsprechende Methode addExtension() auf:

Query q = pm.newQuery(Person.class);
q.addExtension("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

Die möglichen Werte sind "EVENTUAL" und "STRONG" Der Standardwert ist "STRONG", sofern in der Konfigurationsdatei jdoconfig.xml nichts anderes festgelegt ist.

Wenn Sie das Zeitlimit für Datenspeicheraufrufe für eine einzelne Abfrage überschreiben möchten, rufen Sie die entsprechende Methode setDatastoreReadTimeoutMillis() auf:

q.setDatastoreReadTimeoutMillis(3000);

Der Wert ist ein Zeitintervall in Millisekunden.