Überblick über die Python 2 App Engine-NDB-Clientbibliothek

Dank der Google Datastore NDB-Clientbibliothek können Python-Anwendungen in App Engine eine Verbindung mit Datastore herstellen. Die NDB-Clientbibliothek baut auf der früheren DB Datastore-Bibliothek auf und fügt die folgenden Datenspeicherfeatures hinzu:

  • Die Klasse StructuredProperty, die Entitäten eine verschachtelte Struktur ermöglicht
  • Integriertes automatisches Caching, das in der Regel schnelle und kostengünstige Lesevorgänge über einen Kontext-Cache und Memcache ermöglicht
  • Asynchrone APIs für gleichzeitige Aktionen und synchrone APIs werden unterstützt

Diese Seite bietet eine Einführung und einen Überblick über die App Engine NDB-Clientbibliothek. Informationen zur Migration zu Cloud NDB, die Python 3 unterstützt, finden Sie unter Zu Cloud NDB migrieren.

Entitäten, Schlüssel und Attribute definieren

In Datastore werden Datenobjekte gespeichert, sogenannte Entitäten. Eine Entität hat mindestens eine Property, bei der es sich um einen benannten Wert eines von mehreren unterstützten Datentypen handelt. Eine Property kann beispielsweise ein String, eine Ganzzahl oder ein Verweis auf eine andere Entität sein.

Jede Entität wird von einem Schlüssel identifiziert, einer eindeutigen Kennzeichnung im Datenspeicher der Anwendung. Der Schlüssel kann ein übergeordnetes Element haben – einen anderen Schlüssel. Dieses übergeordnete Element wiederum kann selbst ein übergeordnetes Element haben usw. An der Spitze dieser Kette übergeordneter Elemente steht ein Schlüssel ohne übergeordnetes Element. Dieser wird als Stamm bezeichnet.

Zeigt die Beziehung zwischen der Stammentität und den untergeordneten Entitäten in einer Entitätsgruppe

Entitäten, deren Schlüssel die gleiche Wurzel haben, bilden eine Entitätengruppe oder eine Gruppe. Wenn sich Entitäten in verschiedenen Gruppen befinden, erscheinen Änderungen an diesen Entitäten manchmal als unpassend. Wenn die Entitäten in der Semantik Ihrer Anwendung nicht verknüpft sind, ist alles in Ordnung. Sollen die Änderungen einiger Entitäten jedoch konsistent sein, müssen sie von Ihrer Anwendung beim Erstellen derselben Gruppe zugewiesen werden.

Im folgenden Diagramm zu der Beziehung von Entitäten und dem Beispielcode sehen Sie, dass ein Guestbook mehrere Greetings mit eigenen content- und date-Attributen haben kann.

Zeigt Entitätsbeziehungen, die mit dem enthaltenen Codebeispiel erstellt wurden

Diese Beziehung wird im Codebeispiel unten implementiert.

import cgi
import textwrap
import urllib

from google.appengine.ext import ndb

import webapp2

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):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

        greeting_blockquotes = []
        for greeting in greetings:
            greeting_blockquotes.append(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write(textwrap.dedent("""\
            <html>
              <body>
                {blockquotes}
                <form action="/sign?{sign}" method="post">
                  <div>
                    <textarea name="content" rows="3" cols="60">
                    </textarea>
                  </div>
                  <div>
                    <input type="submit" value="Sign Guestbook">
                  </div>
                </form>
                <hr>
                <form>
                  Guestbook name:
                    <input value="{guestbook_name}" name="guestbook_name">
                    <input type="submit" value="switch">
                </form>
              </body>
            </html>""").format(
                blockquotes='\n'.join(greeting_blockquotes),
                sign=urllib.urlencode({'guestbook_name': guestbook_name}),
                guestbook_name=cgi.escape(guestbook_name)))

class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()
        self.redirect('/?' + urllib.urlencode(
            {'guestbook_name': guestbook_name}))

app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', SubmitForm)
])

Modelle zum Speichern von Daten verwenden

Ein Modell ist eine Klasse, die einen Entitätentyp beschreibt. Dies schließt sowohl die Typen als auch die Konfiguration ihrer Properties ein. Das ist in etwa so wie in einer Tabelle in SQL. Eine Entität kann durch Aufrufen des Klassenkonstruktors des Modells erstellt und dann durch Aufrufen der Methode put() gespeichert werden.

Durch diesen Beispielcode wird die Modellklasse Greeting definiert. Jede Entität Greeting hat zwei Attribute: den Text der Begrüßung und das Datum, an dem die Begrüßung erstellt wurde.

class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)
class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()

In der Anwendung wird ein neues Objekt Greeting erstellt und die Methode put() aufgerufen, um eine neue Begrüßung zu erstellen und zu speichern.

Damit keine Gästebuch-Begrüßungen in der falschen Reihenfolge erscheinen, wird in der Anwendung beim Erstellen eines neuen Greeting-Objekts ein übergeordneter Schlüssel festgelegt. Somit befindet sich die neue Begrüßung in derselben Entitätengruppe wie andere Begrüßungen im selben Gästebuch. Die Anwendung profitiert von diesem Umstand durch die Verwendung von Ancestor-Abfragen.

Abfragen und Indexe

In einer Anwendung besteht die Möglichkeit, anhand von Abfragen nach Entitäten zu suchen, die bestimmten Filtern entsprechen.

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

Bei einer typischen NDB-Abfrage werden Entitäten nach Art gefiltert. In diesem Beispiel generiert query_book eine Abfrage, die Greeting-Entitäten zurückgibt. Eine Abfrage kann außerdem Filter für Attributwerte und Schlüssel von Entitäten angeben. Wie in diesem Beispiel kann in einer Abfrage ein Ancestor angegeben werden, sodass nur Entitäten gefunden werden, die zu einem bestimmten Ancestor gehören. In einer Abfrage kann die Sortierreihenfolge angeben werden. Wenn eine bestimmte Entität für alle in den Filtern und Sortierreihenfolgen vorhandenen Properties mindestens einen Wert (inklusive null) hat und wenn die Property-Werte alle Filterkriterien erfüllen, wird die Entität als Ergebnis zurückgegeben.

Alle Abfragen verwenden einen Index, eine Tabelle, die die Ergebnisse der Abfrage in der gewünschten Reihenfolge enthält. Im zugrunde liegenden Datenspeicher werden einfache Indexe automatisch verwaltet (Indexe, die nur eine Property verwenden).

Die komplexen Indexe werden in der Konfigurationsdatei index.yaml definiert. Der Entwicklungs-Webserver fügt dieser Datei automatisch Vorschläge hinzu, wenn Abfragen erkannt werden, für die noch keine Indexe konfiguriert wurden.

Sie können Indexe vor dem Hochladen der Anwendung manuell optimieren, indem Sie die Datei bearbeiten. Außerdem haben Sie die Möglichkeit, die Indexe unabhängig vom Upload der Anwendung zu aktualisieren, wenn Sie gcloud app deploy index.yaml ausführen. Wenn Ihr Datenspeicher viele Entitäten hat, dauert es lang, einen neuen Index für sie zu erstellen. In diesem Fall sollten Sie die Indexdefinitionen vor dem Hochladen von Code aktualisieren, der den neuen Index verwendet. Sie können mithilfe der Verwaltungskonsole feststellen, wann die Erstellung der Indexe abgeschlossen wurde.

Der Indexmechanismus unterstützt ein breites Spektrum an Abfragen und ist für die meisten Anwendungen geeignet. Einige Arten von Abfragen herkömmlicher Datenbanktechnologien werden jedoch nicht unterstützt. Das gilt insbesondere für Joins.

NDB-Schreibvorgänge: Commit ausführen, Cache entwerten und Änderung übernehmen

NDB schreibt Daten schrittweise:

  • In der "Commit"-Phase zeichnet der zugrunde liegende Datenspeicherdienst die Änderungen auf.
  • NDB entwertet die Caches der betroffenen Entitäten. Somit wird bei zukünftigen Lesevorgängen aus dem zugrunde liegenden Datenspeicher gelesen und in diesem zwischengespeichert, anstatt veraltete Werte aus dem Cache zu lesen.
  • Schließlich wendet der zugrunde liegende Datenspeicher die Änderung an – unter Umständen nur Sekunden später. Die Änderung wird für globale Abfragen und schließlich auch für konsistente Lesevorgänge sichtbar.

Die NDB-Funktion, die die Daten schreibt (z. B. put()), wird nach der Cache-Entwertung wieder angezeigt. Die "Übernahme"-Phase verläuft asynchron.

Wenn während der Commit-Phase ein Fehler auftritt, werden automatisch Wiederholungsversuche durchgeführt. Treten die Fehler jedoch weiterhin auf, wird eine Ausnahme an Ihre Anwendung gesendet. Wenn die "Commit"-Phase erfolgreich ist und die "Übernanhme"-Phase fehlschlägt, wird die "Übernahme"-Phase fortgesetzt und abgeschlossen, wenn eines der folgenden Ereignisse eintrifft:

  • Nicht abgeschlossene "Commit"-Jobs werden im Rahmen von regelmäßigen Datenspeicherprüfungen identifiziert und übernommen.
  • Durch die nächsten Schreib-, Transaktions- oder strikt konsistenten Lesevorgänge in der betroffenen Entitätengruppe werden die noch nicht übernommenen Änderungen vor den Lese-, Schreib- oder Transaktionsvorgängen übernommen.

Dieses Verhalten wirkt sich darauf aus, wie und wann Daten für Ihre Anwendung sichtbar sind. Wenn die NDB-Funktion wieder angezeigt wird, wurde die Änderung möglicherweise nach einigen Hundert Millisekunden noch nicht vollständig im zugrunde liegenden Datenspeicher übernommen. Bei einer Nicht-Ancestor-Abfrage, die während der Übernahme einer Änderung durchgeführt wurde, kann ein inkonsistenter Status auftreten. Dieser ist lediglich ein Teil der Änderung.

Transaktionen und Caching-Daten

In der NDB-Clientbibliothek können mehrere Vorgänge in einer einzigen Transaktion zusammengefasst werden. Die Transaktion ist nur erfolgreich, wenn jeder Vorgang in der Transaktion erfolgreich ist. Sollte einer der Vorgänge fehlschlagen, wird die Transaktion automatisch zurückgesetzt. Dies ist besonders für verteilte Webanwendungen nützlich, bei denen mehrere Nutzer gleichzeitig auf dieselben Daten zugreifen oder diese bearbeiten können.

NDB verwendet Memcache als Cache-Dienst für Hotspots in den Daten. Wenn einige Entitäten häufig von der Anwendung gelesen werden, können sie von NDB schnell aus dem Cache gelesen werden.

Django mit NDB verwenden

Fügen Sie dazu google.appengine.ext.ndb.django_middleware.NdbDjangoMiddleware in den Eintrag MIDDLEWARE_CLASSES in Ihrer Django-Datei settings.py ein. Es empfiehlt sich, diese Middleware vor allen anderen Middleware-Klassen einzufügen. Der Grund dafür ist, dass andere Middleware mitunter Datenspeicheraufrufe durchführt, die nicht ordnungsgemäß verarbeitet werden, wenn die andere Middleware vor dieser Middleware aufgerufen wird. Weitere Informationen zu Django-Middleware

Weitere Informationen

Weitere Informationen zu folgenden Themen: