NDB-Abfragen

Eine Anwendung kann mit Abfragen den Datenspeicher nach Entitäten durchsuchen, die mit bestimmten Suchkriterien übereinstimmen. Diese werden als Filter bezeichnet.

Überblick

Eine Anwendung kann mit Abfragen den Datenspeicher nach Entitäten durchsuchen, die mit bestimmten Suchkriterien übereinstimmen. Diese werden als Filter bezeichnet. Beispielsweise könnte eine Anwendung, die mehrere Gästebücher verfolgt, mit einer Abfrage Nachrichten aus einem Gästebuch abrufen, die nach Datum sortiert sind:

from google.appengine.ext import ndb
...
class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)
...
class MainPage(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 20

    def get(self):
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*')
        greetings = Greeting.query_book(ancestor_key).fetch(
            self.GREETINGS_PER_PAGE)

        self.response.out.write('<html><body>')

        for greeting in greetings:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write('</body></html>')

Einige Abfragen sind komplexer als andere. Hierfür benötigt der Datenspeicher vordefinierte Indexe. Diese vordefinierten Indexe werden in der Konfigurationsdatei index.yaml angegeben. Wenn Sie auf dem Entwicklungsserver eine Abfrage ausführen, die einen von Ihnen angegebenen Index benötigt, fügt der Entwicklungsserver diesen automatisch zu seiner index.yaml-Datei hinzu. Auf Ihrer Website schlägt eine Abfrage allerdings fehl, für die ein noch nicht angegebener Index erforderlich ist. Daher wird es bei einem typischen Entwicklungszyklus mit einer neuen Abfrage auf dem Entwicklungsserver versucht. Anschließend wird die Website aktualisiert, damit die automatisch geänderte index.yaml-Datei verwendet wird. Sie können index.yaml getrennt vom Upload der Anwendung aktualisieren, indem Sie gcloud app deploy index.yaml ausführen. Wenn Ihr Datenspeicher viele Entitäten hat, dauert es lange, einen neuen Index für sie zu erstellen. In diesem Fall sollten Sie die Indexdefinitionen aktualisieren, bevor Sie Code hochladen, der den neuen Index verwendet. Sie können mithilfe der Verwaltungskonsole feststellen, wann die Erstellung der Indexe abgeschlossen wurde.

Der App Engine-Datenspeicher unterstützt nativ Filter für genaue Übereinstimmungen (der Operator "==") und Vergleiche (die Operatoren "<", "<=", ">" und ">="). Außerdem wird die Kombination mehrerer Filter mit einem booleschen AND-Vorgang unterstützt. Es gelten einige Einschränkungen (siehe unten).

Neben den nativen Operatoren unterstützt die API auch den !=-Operator, wobei Gruppen von Filtern mit dem booleschen OR-Vorgang und dem IN-Vorgang kombiniert werden. Damit wird die Übereinstimmung mit einem der Liste möglicher Werte überprüft (wie beim "in"-Operator von Python). Diese Vorgänge decken sich nicht genau mit den nativen Vorgängen des Datenspeichers, daher sind sie eher ungewöhnlich und relativ langsam. Bei ihrer Implementierung werden Ergebnisstreams speicherintern zusammengeführt. Beachten Sie, dass p != v als "p < v ODER p > v" implementiert ist. Dies ist für wiederkehrende Attribute relevant.

Einschränkungen: Der Datenspeicher erzwingt bestimmte Einschränkungen für Abfragen. Bei Verstößen werden Ausnahmen ausgelöst. Wenn beispielsweise zu viele Filter mithilfe von Ungleichungen für mehrere Attribute kombiniert werden oder wenn eine Ungleichung mit einer Sortierfolge für ein anderes Attribut kombiniert wird, sind momentan alle Abfragen unzulässig. Bei Filtern, die auf mehrere Attribute verweisen, sind in einigen Fällen sekundäre Indexe für die Konfiguration erforderlich.

Nicht unterstützt: Der Datenspeicher unterstützt keine Teilstring-Übereinstimmungen, Übereinstimmungen, bei denen die Groß-/Kleinschreibung nicht berücksichtigt wird, und keine sogenannten Volltextsuchvorgänge. Es gibt Möglichkeiten, Übereinstimmungen, bei denen die Groß-/Kleinschreibung nicht berücksichtigt wird, und sogar Volltextsuchvorgänge mithilfe berechneter Attribute zu implementieren.

Nach Attribut-Werten filtern

Rufen Sie noch einmal die Klasse Account aus den NDB-Attributen auf:

class Account(ndb.Model):
    username = ndb.StringProperty()
    userid = ndb.IntegerProperty()
    email = ndb.StringProperty()

Normalerweise empfiehlt es sich nicht, alle Entitäten einer bestimmten Art abzurufen. Sie möchten nur diejenigen mit einem bestimmten Wert oder Wertebereich für ein Attribut.

Einige Operatoren werden von Attribut-Objekten überladen, um Filterausdrücke zurückzugeben, die zur Steuerung einer Abfrage verwendet werden können: Wenn Sie beispielsweise alle "Account"-Entitäten finden möchten, deren "userid"-Attribut genau den Wert 42 hat, können Sie den folgenden Ausdruck verwenden:

query = Account.query(Account.userid == 42)

(Wenn Sie sicher sind, dass nur ein Account mit diesem userid vorhanden war, können Sie userid als Schlüssel verwenden. Account.get_by_id(...) ist schneller als Account.query(...).get().)

NDB unterstützt diese Operationen:

property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])

Sie können Syntax wie die folgende verwenden, um nach Ungleichungen zu filtern:

query = Account.query(Account.userid >= 40)

Dadurch werden alle "Account"-Entitäten gefunden, deren userid-Attribut größer oder gleich 40 ist.

Zwei dieser Operationen, != und IN, werden als Kombinationen der anderen implementiert und sind etwas ungewöhnlich, wie unter != und IN beschrieben.

Sie können mehrere Filter angeben:

query = Account.query(Account.userid >= 40, Account.userid < 50)

So werden die angegebenen Filterargumente und alle "Account"-Entitäten zurückgegeben, deren "userid"-Wert größer oder gleich 40 und kleiner als 50 ist.

Hinweis: Wie bereits erwähnt, lehnt der Datenspeicher Abfragen ab, bei denen die Ungleichheitsfilter für mehr als ein Attribut verwendet werden.

Statt einen vollständigen Abfragefilter in einem einzelnen Ausdruck anzugeben, empfiehlt es sich unter Umständen, ihn in mehreren Schritten zu erstellen. Beispiel:

query1 = Account.query()  # Retrieve all Account entitites
query2 = query1.filter(Account.userid >= 40)  # Filter on userid >= 40
query3 = query2.filter(Account.userid < 50)  # Filter on userid < 50 too

query3 entspricht der Variable query aus dem vorherigen Beispiel. Beachten Sie, dass Abfrageobjekte unveränderlich sind. Die Erstellung von query2 wirkt sich also nicht auf query1 und die Erstellung von query3 nicht auf query1 oder query2 aus.

Die Vorgänge != und IN

Rufen Sie noch einmal die Klasse Article aus den NDB-Attributen auf:

class Article(ndb.Model):
    title = ndb.StringProperty()
    stars = ndb.IntegerProperty()
    tags = ndb.StringProperty(repeated=True)

Die Operationen != (ungleich) und IN (Mitgliedschaft) werden mithilfe anderer Filter und der OR-Operation kombiniert. Die erste Operation

property != value

wird so implementiert:

(property < value) OR (property > value)

Beispiel:

query = Article.query(Article.tags != 'perl')

entspricht

query = Article.query(ndb.OR(Article.tags < 'perl',
                             Article.tags > 'perl'))

Hinweis: Bei dieser Abfrage wird nicht nach Article-Entitäten gesucht, die nicht das Tag "perl" haben. Stattdessen werden alle Entitäten mit mindestens einem Tag gefunden, das nicht mit "perl" identisch ist. Beispielsweise würde die folgende Entität nicht in den Ergebnissen auftauchen, obwohl es sich bei einem seiner Tags um "perl" handelt:

Article(title='Perl + Python = Parrot',
        stars=5,
        tags=['python', 'perl'])

Diese Entität wäre allerdings nicht enthalten:

Article(title='Introduction to Perl',
        stars=3,
        tags=['perl'])

Es können keine Entitäten abgefragt werden, die kein Tag enthalten, das mit "perl" identisch ist.

Bei der Operation "IN"

property IN [value1, value2, ...]

wird in einer Liste möglicher Werte nach Mitgliedschaft gesucht. Er wird so implementiert:

(property == value1) OR (property == value2) OR ...

Beispiel:

query = Article.query(Article.tags.IN(['python', 'ruby', 'php']))

entspricht

query = Article.query(ndb.OR(Article.tags == 'python',
                             Article.tags == 'ruby',
                             Article.tags == 'php'))

Hinweis: Abfragen, die OR verwenden, deduplizieren ihre Ergebnisse. Der Ergebnisstream enthält keine Entität mehr als einmal, selbst wenn eine Entität zwei oder mehr Unterabfragen entspricht.

Wiederkehrende Attribute abfragen

Die im vorherigen Abschnitt definierte Klasse Article eignet sich auch als Beispiel für das Abfragen wiederkehrender Attribute. Ein Filter wie

Article.tags == 'python'

verwendet einen einzelnen Wert, obwohl es sich bei Article.tags um ein wiederkehrendes Attribut handelt. Sie können wiederkehrende Attribute nicht vergleichen, um Objekte aufzulisten. Dies könnte vom Datenspeicher nicht verarbeitet werden. Ein Filter wie

Article.tags.IN(['python', 'ruby', 'php'])

sucht nicht nach Article-Entitäten, deren Tag-Wert die Liste ['python', 'ruby', 'php'] ist. Er sucht nach Entitäten, deren tags-Wert (als Liste betrachtet) mindestens einen dieser Werte enthält.

Die Abfrage eines Werts von None für ein wiederholtes Attribut hat ein nicht definiertes Verhalten und sollte nicht ausgeführt werden.

"AND"- und "OR"-Operationen kombinieren

AND- und OR-Vorgänge können beliebig verschachtelt werden. Beispiel:

query = Article.query(ndb.AND(Article.tags == 'python',
                              ndb.OR(Article.tags.IN(['ruby', 'jruby']),
                                     ndb.AND(Article.tags == 'php',
                                             Article.tags != 'perl'))))

Aufgrund der Implementierung von OR kann eine Abfrage in dieser Form mit einer Ausnahme fehlschlagen, weil sie zu komplex ist. Es empfiehlt sich, diese Filter zu normalisieren, sodass es an der Spitze der Ausdrucksstruktur höchstens eine einzelne OR-Operation gibt sowie eine einzelne Ebene von AND-Operationen darunter.

Um diese Normalisierung durchzuführen, müssen Sie sich sowohl Ihre Regeln der booleschen Logik als auch die Implementierung der Filter != und IN merken:

  1. Erweitern Sie die Operatoren != und IN auf ihre einfache Form, wobei != prüft, ob das Attribut kleiner ("<") oder größer (">") als der Wert ist. Mit IN wird geprüft, ob das Attribut mit dem ersten Wert oder dem zweiten Wert usw. bis zum letzten Wert der Liste identisch ("==") ist.
  2. Ein AND mit einem OR darin entspricht einem OR mit mehreren AND, die auf die ursprünglichen AND-Operanden angewendet werden, wobei ein einzelner OR-Operand durch das Original OR ersetzt wird. AND(a, b, OR(c, d)) entspricht beispielsweise OR(AND(a, b, c), AND(a, b, d)).
  3. Ein AND mit einem Operanden, der selbst eine AND-Operation ist, kann die Operanden des verschachtelten AND in den einschließenden AND einbeziehen. AND(a, b, AND(c, d)) entspricht beispielsweise AND(a, b, c, d).
  4. Ein OR mit einem Operanden, der selbst eine OR-Operation ist, kann die Operanden des verschachtelten OR in den einschließenden OR einbeziehen. OR(a, b, OR(c, d)) entspricht beispielsweise OR(a, b, c, d).

Wenn diese Transformationen mit einer einfacheren Schreibweise als Python stufenweise auf den Beispielfilter angewendet werden, erhalten Sie Folgendes:

  1. Anwendung von Regel 1 auf die Operationen IN und !=:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Anwendung von Regel 2 auf das innerste OR, das mit einem AND verschachtelt ist:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Anwendung von Regel 4 auf das OR, das innerhalb eines anderen OR verschachtelt ist:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Anwendung von Regel 2 auf das verbleibende OR, das in einem AND verschachtelt ist:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', AND(tags == 'php', tags < 'perl')),
       AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
  5. Anwendung von Regel 3, um die verbleibenden verschachtelten ANDs zu minimieren:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', tags == 'php', tags < 'perl'),
       AND(tags == 'python', tags == 'php', tags > 'perl'))

Vorsicht: Bei einigen Filtern kann diese Normalisierung zu einer kombinatorischen Explosion führen. Ziehen Sie das AND von drei OR-Klauseln mit jeweils zwei grundlegenden Klauseln in Erwägung. Bei der Normalisierung wird dies ein OR mit acht AND-Klauseln sowie jeweils drei Basisklauseln. Sechs Bedingungen werden also zu 24.

Sortierfolgen angeben

Mit der Methode order() können Sie die Reihenfolge angeben, in der eine Abfrage die Ergebnisse zurückgibt. Diese Methode verwendet eine Liste von Argumenten, von denen jedes entweder ein Attribut-Objekt (in aufsteigender Reihenfolge) oder seine Negation (in absteigender Reihenfolge) ist. Beispiel:

query = Greeting.query().order(Greeting.content, -Greeting.date)

Dadurch werden alle Greeting-Entitäten abgerufen, sortiert nach aufsteigendem Wert ihres content-Attributs. Werden aufeinanderfolgende Entitäten mit demselben "Content"-Attribut ausgeführt, sind sie nach absteigendem Wert ihrer date-Attribute sortiert. Sie können mehrere order()-Aufrufe für denselben Effekt verwenden:

query = Greeting.query().order(Greeting.content).order(-Greeting.date)

Hinweis: Wenn Sie Filter mit order() kombinieren, lehnt der Datenspeicher bestimmte Kombinationen ab. Das gilt insbesondere, wenn Sie einen Ungleichheitsfilter verwenden. Dann muss die erste Sortierfolge (falls vorhanden) dasselbe Attribut wie der Filter angeben. Manchmal müssen Sie auch einen sekundären Index konfigurieren.

Ancestor-Abfragen

Mit Ancestor-Abfragen können Sie strikt konsistente Abfragen für den Datenspeicher erstellen. Entitäten mit demselben Ancestor sind jedoch auf einen Schreibvorgang pro Sekunde beschränkt. Im Folgenden finden Sie einen einfachen Vergleich der Vor- und Nachteile sowie der Struktur von Ancestor- und Nicht-Ancestor-Abfragen. Dabei werden Kunden und die mit ihnen verknüpften Käufe im Datenspeicher verwendet.

Im folgenden Beispiel für eine Nicht-Ancestor-Abfrage gibt es für jeden Customer eine Entität im Datenspeicher und für jeden Purchase eine Entität im Datenspeicher. Der KeyProperty-Wert verweist auf das Element Kunde.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    customer = ndb.KeyProperty(kind=Customer)
    price = ndb.IntegerProperty()

Mit dieser Abfrage finden Sie alle Käufe, die mit dem Kunden verknüpft sind:

purchases = Purchase.query(
    Purchase.customer == customer_entity.key).fetch()

In diesem Fall bietet der Datenspeicher einen hohen Schreibdurchsatz, jedoch nur Eventual Consistency. Wenn ein neuer Kauf hinzugefügt wurde, erhalten Sie möglicherweise veraltete Daten. Dieses Verhalten können Sie mit Ancestor-Abfragen umgehen.

Für Kunden und Käufe mit Ancestor-Abfragen haben Sie immer noch dieselbe Struktur mit zwei separaten Entitäten. Der Kundenteil ist identisch. Wenn Sie jedoch Käufe erstellen, müssen Sie die KeyProperty() für Käufe nicht mehr angeben. Dies liegt daran, dass Sie beim Verwenden von Ancestor-Abfragen den Schlüssel der Kundenentität aufrufen, wenn Sie eine Kaufentität erstellen.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    price = ndb.IntegerProperty()

Jeder Kauf hat einen Schlüssel und auch der Kunde hat seinen eigenen Schlüssel. In jedem Kaufschlüssel ist jedoch der Schlüssel von "customer_entity" enthalten. Dies wird auf einen Schreibvorgang pro Vorfahr und Sekunde beschränkt. So wird eine Entität mit einem Ancestor erstellt:

purchase = Purchase(parent=customer_entity.key)

Verwenden Sie die folgende Abfrage, um die Käufe eines bestimmten Kunden abzufragen.

purchases = Purchase.query(ancestor=customer_entity.key).fetch()

Abfrageattribute

Abfrageobjekte haben die folgenden schreibgeschützten Datenattribute:

Attribut Typ Standardeinstellung Beschreibung
kind str None Name der Art (in der Regel der Klassenname)
ancestor Key None Zum Abfragen angegebener Ancestor
filters FilterNode None Filterausdruck
orders Order None Sortierfolgen

Durch das Drucken eines Abfrageobjekts (oder das Aufrufen von str() oder repr() darauf) wird eine formatierte Stringdarstellung gedruckt:

print(Employee.query())
# -> Query(kind='Employee')
print(Employee.query(ancestor=ndb.Key(Manager, 1)))
# -> Query(kind='Employee', ancestor=Key('Manager', 1))

Nach Werten für strukturierte Attribute filtern

Eine Abfrage kann direkt nach den Feldwerten strukturierter Attribute filtern. Beispielsweise würde eine Abfrage für alle Kontakte mit einer Adresse, deren Stadt 'Amsterdam' ist, so aussehen:

query = Contact.query(Contact.addresses.city == 'Amsterdam')

Wenn Sie mehrere Filter dieser Art kombinieren, stimmen die Filter möglicherweise mit verschiedenen Address-Unterentitäten innerhalb derselben "Contact"-Entität überein. Beispiel:

query = Contact.query(Contact.addresses.city == 'Amsterdam',  # Beware!
                      Contact.addresses.street == 'Spear St')

kann Kontakte mit einer Adresse finden, die die Stadt 'Amsterdam' enthält, und eine andere Adresse mit der Straße 'Spear St'. Sie haben zumindest für Gleichheitsfilter die Möglichkeit, eine Abfrage zu erstellen, die nur Ergebnisse mit mehreren Werten in einer einzigen Unterentität zurückgibt:

query = Contact.query(Contact.addresses == Address(city='San Francisco',
                                                   street='Spear St'))

Wenn Sie diese Technik verwenden, werden Eigenschaften der Untereinheit gleich None in der Abfrage ignoriert. Wenn ein Attribut einen Standardwert hat, müssen Sie es ausdrücklich auf None setzen, damit es in der Abfrage ignoriert wird. Andernfalls enthält die Abfrage einen Filter, bei dem der Attributwert mit der Standardeinstellung identisch sein muss. Wenn das Address-Modell z. B. ein Land mit der Eigenschaft country mit default='us' enthielte, würde das obige Beispiel nur Kontakte mit einem Land gleich 'us' zurückgeben. Um Kontakte mit anderen Länderwerten zu berücksichtigen, müssten Sie nach Address(city='San Francisco', street='Spear St', country=None) filtern.

Wenn eine Unterentität Attributwerte hat, die mit None identisch sind, werden diese ignoriert. Daher macht es keinen Sinn, nach einem Unter-Eigenschaftswert von None zu filtern.

Vom String benannte Attribute verwenden

Sie können eine Abfrage basierend auf einem Attribut filtern oder sortieren, dessen Name durch einen String angegeben wird. Wenn Sie beispielsweise den Nutzer Suchanfragen wie tags:python eingeben lassen, wäre es praktisch, wenn dies in eine Abfrage wie diese umgewandelt werden würde:

Article.query(Article."tags" == "python") # does NOT work

Wenn Ihr Modell ein Expando ist, kann Ihr Filter GenericProperty verwenden; die Klasse, die Expando für dynamische Attribute verwendet:

property_to_query = 'location'
query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF')

Die Verwendung von GenericProperty funktioniert auch, wenn Ihr Modell kein Expando ist. Aber wenn Sie sicherstellen möchten, dass Sie nur definierte Attributnamen verwenden, können Sie auch das _properties-Klassenattribut verwenden.

query = Article.query(Article._properties[keyword] == value)

Sie rufen es mit getattr() aus der Klasse ab:

query = Article.query(getattr(Article, keyword) == value)

Der Unterschied besteht darin, dass getattr() den "Python-Namen" des Attributs verwendet, während _properties durch den "Datenspeichernamen" des Attributs indexiert wird. Diese unterscheiden sich nur, wenn das Attribut in etwa so deklariert wurde:

class ArticleWithDifferentDatastoreName(ndb.Model):
    title = ndb.StringProperty('t')

Hier lautet der Python-Name title, aber der Name des Datenspeichers ist t.

Diese Ansätze sind auch für das Sortieren von Abfrageergebnissen geeignet:

expando_query = FlexEmployee.query().order(ndb.GenericProperty('location'))

property_query = Article.query().order(Article._properties[keyword])

Abfrage-Iteratoren

Während eine Abfrage ausgeführt wird, wird ihr Status in einem Iterator-Objekt gespeichert. Die meisten Anwendungen verwenden sie nicht direkt. In der Regel ist es einfacher, fetch(20) aufzurufen, als das Iteratorobjekt zu bearbeiten. Es gibt zwei grundlegende Möglichkeiten, ein solches Objekt abzurufen:

  • Mit der integrierten Python-Funktion iter() für ein Query-Objekt
  • Durch Aufrufen der Methode iter() des Query-Objekts

Bei der ersten Möglichkeit wird die Verwendung einer for-Schleife von Python unterstützt. Hier wird die iter()-Funktion implizit aufgerufen, um eine Abfrage zu umgehen.

for greeting in greetings:
    self.response.out.write(
        '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

Bei der zweiten Möglichkeit mit der Methode iter() des Query-Objekts können Sie Optionen an den Iterator übergeben, um dessen Verhalten zu beeinflussen. Um beispielsweise eine schlüsselbasierte Abfrage in einer for-Schleife zu verwenden, können Sie Folgendes schreiben:

for key in query.iter(keys_only=True):
    print(key)

Abfrage-Iteratoren bieten weitere nützliche Methoden:

Method Beschreibung
__iter__() Teil des Iterator-Protokolls von Python:
next() Gibt das nächste Ergebnis zurück oder löst die Ausnahme StopIterationStopIteration aus, wenn keines vorhanden ist.

has_next() Gibt True zurück, wenn ein folgender next()-Aufruf ein Ergebnis zurückgibt, und False, wenn StopIteration ausgelöst wird.

Blockiert so lang, bis die Antwort auf diese Frage bekannt ist, und speichert das Ergebnis (falls vorhanden) zwischen, bis Sie es mit next() abrufen.
probably_has_next() Wie has_next(), doch es wird eine schnellere (und manchmal ungenaue) Verknüpfung verwendet.

Gibt möglicherweise eine falsch positive Meldung zurück (True, wenn next() eine StopIteration auslösen würde), aber niemals eine falsch negative (False, wenn next() ein Ergebnis zurückgeben würde).
cursor_before() Gibt einen Abfragecursor zurück, der einen Punkt unmittelbar vor dem letzten zurückgegebenen Ergebnis darstellt.

Löst eine Ausnahme aus, wenn kein Cursor verfügbar ist, insbesondere wenn die Abfrageoption produce_cursors nicht übergeben wurde.
cursor_after() Gibt einen Abfragecursor zurück, der einen Punkt unmittelbar nach dem letzten zurückgegebenen Ergebnis darstellt.

Löst eine Ausnahme aus, wenn kein Cursor verfügbar ist, insbesondere wenn die Abfrageoption produce_cursors nicht übergeben wurde.
index_list() Gibt eine Liste der Indexe zurück, die von einer ausgeführten Abfrage verwendet werden, unter anderem Hauptindexe, zusammengesetzte Indexe, Artindexe sowie Indexe mit einzelnen Attributen.

Abfrage-Cursor

Ein Abfrage-Cursor ist eine kleine intransparente Datenstruktur, die einen Wiederaufnahmepunkt in einer Abfrage darstellt. Sie ist nützlich, wenn einem Nutzer Ergebnisseiten nacheinander gezeigt werden, und eignet sich außerdem, um lange Jobs zu bearbeiten, die unter Umständen angehalten und fortgesetzt werden müssen. Eine typische Methode ist die Verwendung der fetch_page()-Methode einer Abfrage. Sie funktioniert ähnlich wie fetch(), gibt jedoch ein dreifaches (results, cursor, more) zurück. Das zurückgegebene Flag more gibt an, dass wahrscheinlich mehr Ergebnisse vorliegen. Eine Benutzeroberfläche kann dies beispielsweise verwenden, um eine Schaltfläche oder einen Link "Nächste Seite" zu unterdrücken. Um nachfolgende Seiten anzufordern, übergeben Sie den von einem fetch_page()-Aufruf zurückgegebenen Cursor an den nächsten. Wenn Sie einen ungültigen Cursor übergeben, wird eine BadArgumentError ausgelöst. Bei der Validierung wird nur geprüft, ob der Wert base64-codiert ist. Weitere erforderliche Validierungen müssen von Ihnen durchgeführt werden.

Damit der Nutzer alle Entitäten Seite für Seite aufrufen kann, die mit einer Abfrage übereinstimmen, muss Ihr Code ungefähr so aussehen:

from google.appengine.datastore.datastore_query import Cursor
...
class List(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 10

    def get(self):
        """Handles requests like /list?cursor=1234567."""
        cursor = Cursor(urlsafe=self.request.get('cursor'))
        greets, next_cursor, more = Greeting.query().fetch_page(
            self.GREETINGS_PER_PAGE, start_cursor=cursor)

        self.response.out.write('<html><body>')

        for greeting in greets:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        if more and next_cursor:
            self.response.out.write('<a href="/list?cursor=%s">More...</a>' %
                                    next_cursor.urlsafe())

        self.response.out.write('</body></html>')

Beachten Sie, dass der Cursor mithilfe von urlsafe() und Cursor(urlsafe=s) serialisiert und deserialisiert wird. Dadurch können Sie im Web einen Cursor als Antwort auf eine Anfrage an einen Client übergeben und vom Client in einer späteren Anfrage zurückerhalten.

Hinweis: Die Methode fetch_page() gibt in der Regel auch dann einen Cursor zurück, wenn keine weiteren Ergebnisse vorhanden sind. Doch das ist nicht garantiert. Der zurückgegebene Cursorwert kann None sein. Beachten Sie auch dies: Da das Flag more mit der Methode probably_has_next() des Iterators implementiert wird, kann in seltenen Fällen True zurückgegeben werden, obwohl die nächste Seite leer ist.

Abfragecursors werden von einigen NDB-Abfragen nicht unterstützt. Sie können sie jedoch korrigieren. Wenn eine Abfrage IN, OR oder != verwendet, funktionieren die Abfrageergebnisse nicht mit Cursors, es sei denn, sie sind nach Schlüsseln geordnet. Wenn eine Anwendung die Ergebnisse nicht nach Schlüsseln sortiert und fetch_page() aufruft, erhält sie einen BadArgumentError. Wenn User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N) einen Fehler erhält, ändern Sie die Abfrage in User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N).

Anstatt durch Abfrageergebnisse zu "blättern", können Sie die iter()-Methode einer Abfrage verwenden, um einen Cursor an einem bestimmten Punkt zu erhalten. Übergeben Sie dazu produce_cursors=True an iter(): Wenn sich der Iterator an der richtigen Stelle befindet, rufen Sie cursor_after() auf, um einen direkt dahinter liegenden Cursor zu erhalten. Oder rufen Sie cursor_before() auf, um einen Cursor direkt davor zu erhalten. Beachten Sie, dass beim Aufrufen von cursor_after() oder cursor_before() ein blockierender Datastore-Aufruf erfolgen kann, bei dem ein Teil der Abfrage noch einmal ausgeführt wird, um einen Cursor zu extrahieren, der auf die Mitte eines Batches verweist.

Erstellen Sie eine umgekehrte Abfrage, um mit einem Cursor rückwärts durch die Abfrageergebnisse zu blättern:

# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
q_reverse = q.order(-Bar.key)

# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)

# Fetch the same page going backward.
r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor)

Funktion für jede Entity aufrufen ("Zuordnung")

Angenommen, Sie müssen die Account-Entitäten abrufen, die den von einer Abfrage zurückgegebenen Message-Entitäten entsprechen. Sie könnten etwa dies schreiben:

message_account_pairs = []
for message in message_query:
    key = ndb.Key('Account', message.userid)
    account = key.get()
    message_account_pairs.append((message, account))

Dies ist jedoch ziemlich ineffizient. Es wird zuerst darauf gewartet, eine Entität abzurufen. Anschließend wird diese Entität verwendet, bevor dann auf die nächste Entität gewartet wird, um auch diese zu verwenden. Das sind lange Wartezeiten. Eine andere Möglichkeit besteht darin, eine Callback-Funktion zu schreiben, die über die Abfrageergebnisse zugeordnet wird:

def callback(message):
    key = ndb.Key('Account', message.userid)
    account = key.get()
    return message, account

message_account_pairs = message_query.map(callback)
# Now message_account_pairs is a list of (message, account) tuples.

Diese Version läuft etwas schneller als die einfache for-Schleife oben, da es zu Gleichzeitigkeiten kommen kann. Da der Aufruf von get() in callback() jedoch immer noch synchron ist, ist die Verbesserung nicht besonders groß. Dies ist eine gute Gelegenheit, um asynchrone "Gets" zu verwenden.

GQL

GQL ist eine SQL-ähnliche Sprache zum Abrufen von Entitäten oder Schlüsseln aus dem App Engine-Datenspeicher. Die Funktionen von GQL weichen zwar von jenen einer Anfragesprache für herkömmliche relationale Datenbanken ab, die Syntax von GQL ist der von SQL jedoch ähnlich. Die GQL-Syntax wird in der GQL-Referenz beschrieben.

Sie können mit GQL Abfragen erstellen. Das ist mit dem Erstellen einer Abfrage mit Model.query() vergleichbar. Allerdings wird die GQL-Syntax zum Definieren des Abfragefilters und der Reihenfolge verwendet. Dabei gilt:

  • ndb.gql(querystring) gibt ein Query-Objekt zurück (denselben Typ wie von Model.query() zurückgegeben). Für Query-Objekte wie fetch(), map_async(), filter() stehen alle üblichen Methoden zur Verfügung.
  • Model.gql(querystring) ist eine Abkürzung für ndb.gql("SELECT * FROM Model " + querystring). In der Regel ist querystring etwas wie "WHERE prop1 > 0 AND prop2 = TRUE".
  • Wenn Sie Modelle abfragen möchten, die strukturierte Attribute enthalten, können Sie in Ihrer GQL-Syntax mit foo.bar auf Unterattribute verweisen.
  • GQL unterstützt SQL-ähnliche Parameterbindungen. Eine Anwendung kann Abfragen definieren und dann Werte einbinden:
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    oder
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    Durch das Aufrufen der Funktion bind() wird eine neue Abfrage zurückgegeben. Das Original wird nicht geändert.

  • Wenn Ihre Modellklasse die Klassenmethode _get_kind() überschreibt, sollte Ihre GQL-Abfrage die von dieser Funktion zurückgegebene Art und nicht den Klassennamen verwenden.
  • Wenn ein Attribut in Ihrem Modell seinen Namen überschreibt, z. B. foo = StringProperty('bar')) sollte Ihre GQL-Abfrage den überschriebenen Attributenamen verwenden (im Beispiel bar).

Verwenden Sie immer die Funktion zum Binden von Parametern, wenn es sich bei einigen der Werte in Ihrer Abfrage um Variablen handelt, die vom Nutzer bereitgestellt wurden. So werden Angriffe auf der Grundlage von syntaktischen Hacks verhindert.

Es ist ein Fehler, eine Abfrage für ein Modell auszuführen, das nicht importiert (oder definiert) wurde.

Es ist ein Fehler, einen Attributnamen zu verwenden, der nicht von der Modellklasse definiert wurde, es sei denn, dieses Modell ist ein Expando.

Durch das Angeben eines Limits oder Offsets für die fetch()-Methode der Abfrage wird das Limit oder Offset überschrieben, das von den GQL-Klauseln OFFSET und LIMIT festgelegt wurde. Kombinieren Sie OFFSET und LIMIT von GQL nicht mit fetch_page(). Beachten Sie, dass die von App Engine auf Abfragen festgelegte Höchstzahl von 1.000 Ergebnissen sowohl für Offset als auch für Limit gilt.

Wenn Sie mit SQL vertraut sind, sollten Sie bei der Verwendung von GQL auf falsche Annahmen achten. GQL wird in die native Abfrage-API von NDB übersetzt. Dies unterscheidet sich von einem typischen Object-Relational-Mapper wie SQLAlchemy oder der Datenbankunterstützung von Django, bei dem die API-Aufrufe in SQL übersetzt werden, bevor sie an den Datenbankserver übertragen werden. GQL unterstützt keine Datenspeicheränderungen ("inserts", "deletes" oder "updates"), sondern nur Abfragen.