App Engine Datastore API für gebündelte Legacy-Dienste

In diesem Artikel werden das Datenmodell für in Datastore gespeicherte Objekte, die Strukturierung von Abfragen mithilfe der API und die Verarbeitung von Transaktionen erläutert. Informationen zum Inhalt des Pakets datastore finden Sie in der datastore-Paketreferenz.

Entitäten

Objekte in Datastore werden als Entitäten bezeichnet. Eine Entität hat ein oder mehrere benannte Attribute, von denen jedes einen oder mehrere Werte haben kann. Die Attributwerte unterstützen eine Vielzahl von Datentypen, darunter Ganzzahlwerte, Gleitkommazahlenwerte, Strings, Datumswerte und Binärdaten. Mit einer Abfrage für ein Attribut mit mehreren Werten wird getestet, ob einer der Werte die Abfragekriterien erfüllt. Diese Attribute eignen sich daher zum Testen von Mitgliedschaften.

Typen, Schlüssel und Kennungen

Jede Datastore-Entität ist von einer bestimmten Art. Mit dieser kann die Entität für Abfragen kategorisiert werden. In einer Personalanwendung kann beispielsweise jeder Mitarbeiter eines Unternehmens mit einer Entität der Art Employee dargestellt werden. Darüber hinaus wird jede Entität durch einen eigenen Schlüssel eindeutig identifiziert. Der Schlüssel besteht aus den folgenden Komponenten:

  • Dem Typ der Entität
  • Einer Kennung, die Folgendes sein kann:
    • ein Schlüsselnamen-String
    • eine ganzzahlige ID
  • Optionaler Ancestor-Pfad zur Entität innerhalb der Datastore-Hierarchie

Die Kennung wird beim Erstellen der Entität zugewiesen. Weil die Kennung Teil des Schlüssels der Entität ist, wird sie permanent mit der Entität verknüpft und kann nicht geändert werden. Es gibt zwei Möglichkeiten für die Zuweisung:

  • Die Anwendung kann ihren eigenen Schlüsselnamen-String für die Entität angeben.
  • Datastore kann der Entität automatisch eine ganzzahlige numerische ID zuweisen.

Kennungen zuweisen

Cloud Datastore kann so konfiguriert werden, dass automatische IDs mithilfe von zwei verschiedenen Richtlinien für automatische IDs generiert werden:

  • Die default-Richtlinie generiert eine Sequenz von IDs, die annähernd gleichmäßig verteilt sind. Jede ID kann bis zu 16 Ziffern lang sein.
  • Die Richtlinie legacy erstellt eine Abfolge nicht aufeinanderfolgender IDs aus kleineren Ganzzahlen.

Wenn Sie die Entitäts-IDs für den Nutzer anzeigen möchten und/oder deren Reihenfolge wichtig ist, ist eine manuelle Zuordnung die beste Lösung.

Ancestor-Pfade

Entitäten in Cloud Datastore sind hierarchisch organisiert, ähnlich der Verzeichnisstruktur eines Dateisystems. Wenn Sie eine Entität erstellen, können Sie optional eine weitere Entität als übergeordnetes Element angeben. Die neue Entität ist dann ein untergeordnetes Element der übergeordneten Entität. Im Gegensatz zu einem Dateisystem muss die übergeordnete Entität nicht tatsächlich vorhanden sein. Eine Entität ohne übergeordnetes Element wird als Stammentität bezeichnet. Die Verknüpfung zwischen einer Entität und ihrer übergeordneten Entität ist dauerhaft und kann nicht geändert werden, nachdem die Entität erstellt wurde. Cloud Datastore weist zwei Entitäten mit derselben übergeordneten Entität oder zwei Stammentitäten (Entitäten ohne übergeordnete Entität) niemals dieselbe numerische ID zu.

Alle übergeordneten Elemente einer Entität werden als ihre Ancestors bezeichnet und alle untergeordneten Entitäten sind ihre Nachfolger. Eine Stammentität und alle ihre Nachfolger gehören zu derselben Entitätengruppe. Die Abfolge der Entitäten, von einer Stammentität über die untergeordneten Elemente bis zu einer bestimmten Entität, bildet den Ancestor-Pfad. Der vollständige Schlüssel, der die Entität identifiziert, besteht aus einer Abfolge von Art/Kennungs-Paaren, die den Ancestor-Pfad angeben und mit dem Paar der Entität selbst enden:

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

Bei einer Stammentität ist der Ancestor-Pfad leer und der Schlüssel besteht ausschließlich aus der eigenen Art und der eigenen Kennung der Entität:

[Person:GreatGrandpa]

Dieses Konzept wird anhand des folgenden Diagramms veranschaulicht:

Zeigt die Beziehung der Stammentität zu den untergeordneten Entitäten in der Entitätengruppe an

Abfragen und Indexe

Neben dem direkten Abrufen von Entitäten aus Datastore über die entsprechenden Schlüssel kann eine Anwendung die Entitäten auch über eine Abfrage nach den Werten ihrer Attribute abrufen. Die Abfrage wird für Entitäten eines bestimmten Typs ausgeführt. Sie kann Filter für die Attributwerte, Schlüssel und Ancestors der Entitäten angeben sowie 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 für jede Property, die in den Filtern und Sortierfolgen angegeben wird, und deren Propertywerte 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.

Eine Abfrage kann auch einen Ancestor-Filter enthalten, der die Ergebnisse auf die Entitätengruppe beschränkt, die aus Nachfolgern eines angegebenen Ancestors besteht. Eine solche Abfrage wird als Ancestor-Abfrag bezeichnet. Standardmäßig liefern Ancestor-Abfragen Ergebnisse mit strikter Konsistenz, die verlässlich die neuesten Änderungen an den Daten widerspiegeln. Nicht-Ancestor-Abfragen können sich im Gegensatz dazu auf den gesamten Datastore und nicht nur auf eine einzelne Entitätengruppe beziehen. Sie bieten jedoch nur Eventual Consistency und können veraltete Ergebnisse liefern. Wenn strikte Konsistenz für Ihre Anwendung wichtig ist, sollten Sie dies beim Strukturieren der Daten möglicherweise berücksichtigen und verwandte Entitäten in derselben Entitätengruppe platzieren, damit sie mit einer Ancestor-Abfrage anstatt einer Nicht-Ancestor-Abfrage abgerufen werden können, um weitere Informationen zu erhalten.

App Engine gibt für jede Property einer Entität einen einfachen Index vor. Eine App Engine-Anwendung kann weitere benutzerdefinierte Indexe in der Indexkonfigurationsdatei index.yaml definieren. Der Entwicklungsserver fügt dieser Datei automatisch Vorschläge hinzu, wenn er Abfragen ermittelt, die mit den vorhandenen Indexen nicht ausgeführt werden können. Sie können Indexe vor dem Hochladen der Anwendung manuell optimieren, indem Sie die Datei bearbeiten.

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.

Transaktionen

Jeder Versuch, eine Entität zu erstellen, zu aktualisieren oder zu löschen, findet im Kontext einer Transaktion statt. Eine einzelne Transaktion kann eine beliebige Anzahl solcher Vorgänge umfassen. Um die Konsistenz der Daten zu wahren, sorgt die Transaktion dafür, dass entweder alle enthaltenen Vorgänge auf Datastore als Einheit oder überhaupt nicht angewendet werden, sofern einer der Vorgänge fehlgeschlagen ist.

Sie können innerhalb einer einzelnen Transaktion mehrere Aktionen für eine Entität ausführen. Wenn Sie beispielsweise den Wert eines Zählerfelds in einem Objekt erhöhen möchten, lesen Sie den Wert des Zählers ab, berechnen den neuen Wert und speichern diesen. Ohne eine Transaktion kann es passieren, dass ein anderer Prozess den Zähler zwischen dem Zeitpunkt, zu dem Sie den Wert ablesen, und dem Zeitpunkt, zu dem Sie ihn aktualisieren, erhöht. Der aktualisierte Wert wird in diesem Fall von Ihrer Anwendung überschrieben. Wenn das Lesen, Berechnen und Schreiben in einer einzigen Transaktion erfolgt, kann die Inkrementierung von keinem anderen Prozess gestört werden.

Transaktionen und Entitätengruppen

Innerhalb einer Transaktion sind nur Ancestor-Abfragen zulässig. Jede Transaktionsabfrage muss somit auf eine einzelne Entitätengruppe beschränkt sein. Die Transaktion selbst lässt sich auf mehrere Entitäten anwenden. Diese können entweder zu einer einzelnen Entitätengruppe oder, bei einer gruppenübergreifenden Transaktion, zu maximal 25 verschiedenen Entitätengruppen gehören.

Datastore verwendet zur Verwaltung von Transaktionen Optimistic Concurrency. Wenn durch zwei oder mehr Transaktionen gleichzeitig versucht wird, eine Entitätengruppe zu ändern (indem vorhandene Entitäten aktualisiert oder neue Entitäten erstellt werden), ist nur die erste mit Commit gespeicherte Transaktion erfolgreich. Bei allen anderen schlägt der Commit fehl. Diese anderen Transaktionen können dann mit den aktualisierten Daten wiederholt werden. Beachten Sie, dass dadurch die Anzahl gleichzeitiger Schreibvorgänge begrenzt wird, die Sie für eine Entität in einer bestimmten Entitätengruppe ausführen können.

Gruppenübergreifende Transaktionen

Eine Transaktion für Entitäten, die zu verschiedenen Entitätengruppen gehören, wird als gruppenübergreifende (XG-)Transaktion bezeichnet. Die Transaktion kann auf maximal 25 Entitätengruppen angewendet werden und ist erfolgreich, solange sich keine weitere gleichzeitige Transaktion auf eine der relevanten Entitätengruppen bezieht. So erhalten Sie mehr Flexibilität bei der Organisation Ihrer Daten: Sie sind nicht gezwungen, verschiedenartige Daten unter demselben Ancestor zu gruppieren, nur um atomare Schreibvorgänge für sie durchzuführen.

In XG-Transaktionen ist es ebenso wenig wie in Einzelgruppentransaktionen möglich, Nicht-Ancestor-Abfragen auszuführen. Sie können jedoch Ancestor-Abfragen für separate Entitätengruppen durchführen. Bei nicht-transaktionalen Abfragen (Nicht-Ancestor-Abfragen) können alle, einige oder gar keine Ergebnisse einer zuvor mit Commit durchgeführten Transaktion angezeigt werden. Weitere Informationen zu diesem Thema finden Sie unter Datastore-Schreibvorgänge und Datentransparenz. Bei solchen nicht-transaktionalen Abfragen ist es jedoch wahrscheinlicher, dass die Ergebnisse einer teilweise mit Commit ausgeführten XG-Transaktion angezeigt werden, als die einer teilweise mit Commit ausgeführten Einzelgruppentransaktion.

Die Leistung und Kosten einer XG-Transaktion, die nur eine einzige Entitätengruppe betrifft, sind genauso hoch wie bei einer Nicht-XG-Einzelgruppentransaktion. Bei einer XG-Transaktion, die mehrere Entitätengruppen betrifft, sind die Betriebskosten genauso hoch wie bei einer Nicht-XG-Transaktion, die Latenz kann jedoch höher sein.

Datastore-Lesevorgänge und Datensichtbarkeit

Daten werden in zwei Phasen in Datastore geschrieben:

  1. In der Commit-Phase werden die Entitätsdaten in den Transaktions-Logs einer Mehrzahl von Replikaten erfasst. Alle Replikate, in denen sie nicht erfasst wurden, werden markiert, damit erkennbar ist, dass ihre Logs nicht aktuell sind.
  2. Die Apply-Phase findet in jedem Replikat unabhängig statt und besteht aus zwei parallel ausgeführten Aktionen:
    • Die Entitätsdaten werden in dieses Replikat geschrieben.
    • Die Indexzeilen für die Entität werden in dieses Replikat geschrieben. Hinweis: Dies kann länger dauern als das Schreiben der Daten selbst.

Der Schreibvorgang wird unmittelbar nach der Commit-Phase wieder aufgenommen. Die Apply-Phase findet dann asynchron statt, möglicherweise zu verschiedenen Zeiten in jedem Replikat und möglicherweise mit Verzögerungen von ein paar hundert Millisekunden oder mehr nach Abschluss der Commit-Phase. Tritt während der Commit-Phase ein Fehler auf, werden automatische Wiederholungen durchgeführt. Wenn Fehler jedoch weiterhin auftreten, gibt Datastore eine Fehlermeldung zurück, die von Ihrer Anwendung als Ausnahme empfangen wird. Wenn die Commit-Phase erfolgreich ist, die Apply-Phase jedoch bei einem bestimmten Replikat fehlschlägt, wird in diesem Replikat ein Rollforward für den Apply-Vorgang bis zum Abschluss ausgeführt, wenn eine der beiden folgenden Situationen eintritt:

  • Nicht abgeschlossene "Commit"-Jobs werden im Rahmen von regelmäßigen Datastore-Prüfungen identifiziert und angewendet.
  • Bestimmte Vorgänge (get, put, delete und Ancestor-Abfragen), die die betroffene Entitätengruppe nutzen, bewirken die Übernahme von mit Commit bestätigten, aber noch nicht angewendeten Änderungen in dem Replikat, in dem sie ausgeführt werden. Erst dann wird mit dem nächsten Vorgang fortgefahren.

Dieses Schreibverhalten kann verschiedene Auswirkungen darauf haben, wie und wann Daten für Ihre Anwendung bei verschiedenen Teilen der Commit- und Apply-Phasen sichtbar sind:

  • Wenn ein Schreibvorgang einen Zeitüberschreitungsfehler meldet, kann (ohne zu versuchen, die Daten zu lesen) nicht ermittelt werden, ob der Vorgang erfolgreich war oder fehlgeschlagen ist.
  • Da Datastore ausstehende Änderungen abruft und Ancestor-Abfragen die Änderungen auf das Replikat anwenden, auf dem sie ausgeführt werden, erhalten diese Vorgänge immer eine konsistente Übersicht aller vorherigen erfolgreichen Transaktionen. Das bedeutet, dass bei einem get-Vorgang (Suchen einer aktualisierten Entität anhand ihres Schlüssels) garantiert die neueste Version dieser Entität gefunden wird.
  • Nicht-Ancestor-Abfragen können veraltete Ergebnisse liefern, da sie möglicherweise auf einem Replikat ausgeführt werden, auf dem die neuesten Transaktionen noch nicht angewendet wurden. Das kann auch beim Ausführen eines Vorgangs auftreten, bei dem ausstehende Transaktionen garantiert angewendet werden, da die Abfrage möglicherweise auf einem anderen Replikat als der vorherige Vorgang ausgeführt wird.
  • Das Timing von gleichzeitigen Änderungen kann sich auf die Ergebnisse von Nicht-Ancestor-Abfragen auswirken. Wenn eine Entität anfangs eine Abfrage erfüllt, dies aber aufgrund einer Änderung später nicht mehr der Fall ist, kann die Entität dennoch in den Ergebnissen der Abfrage enthalten sein, wenn die Änderungen noch nicht auf die Indexe des Replikats angewendet wurden, auf dem die Abfrage ausgeführt wurde.

Datastore-Statistiken

Datastore erfasst Statistiken zu den gespeicherten Daten einer Anwendung, wie die Anzahl der Entitäten pro Dateityp oder wie viel Speicherplatz für Attributwerte eines bestimmten Typs verwendet wird. Sie können diese Statistiken auf der Seite Datastore-Dashboard der Google Cloud Console einsehen. Sie können mit der Datastore API auch programmgesteuert innerhalb der Anwendung auf diese Werte zugreifen. Führen Sie dazu Abfragen nach benannten Entitäten aus. Weitere Informationen finden Sie unter Datastore-Statistiken in Go 1.11.

Go 1.11 Datastore-Beispiel

In Go werden Datastore-Entitäten aus struct-Werten erstellt. Die Felder der Struktur werden zu den Attributen der Entität. Zum Anlegen einer neuen Entität richten Sie den Wert ein, den Sie speichern möchten, erstellen einen Schlüssel und übergeben beide an datastore.Put(). Bei der Aktualisierung einer bestehenden Entität wird ein anderer Put()-Vorgang mit demselben Schlüssel durchgeführt. Zum Abrufen einer Entität aus Datastore richten Sie zuerst einen Wert ein, in den ein Unmarshalling der Entität durchgeführt wird. Anschließend übergeben Sie einen Schlüssel und einen Verweis auf diesen Wert an datastore.Get().

In diesem Beispiel werden einige Daten gespeichert und aus Datastore abgerufen:

import (
	"fmt"
	"net/http"
	"time"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/user"
)

type Employee struct {
	Name     string
	Role     string
	HireDate time.Time
	Account  string
}

func handle(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	e1 := Employee{
		Name:     "Joe Citizen",
		Role:     "Manager",
		HireDate: time.Now(),
		Account:  user.Current(ctx).String(),
	}

	key, err := datastore.Put(ctx, datastore.NewIncompleteKey(ctx, "employee", nil), &e1)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var e2 Employee
	if err = datastore.Get(ctx, key, &e2); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Stored and retrieved the Employee named %q", e2.Name)
}

Weitere Informationen finden Sie in der Referenz zu Datastore.