Datenmodellierung in Python

Hinweis: Entwicklern von neuen Anwendungen wird dringend empfohlen, die NDB-Clientbibliothek zu verwenden. Diese bietet im Vergleich zur vorliegenden Clientbibliothek verschiedene Vorteile, z. B. das automatische Caching von Entitäten über die Memcache API. Wenn Sie derzeit die ältere DB-Clientbibliothek verwenden, finden Sie weitere Informationen im Leitfaden zur Migration von DB- zu NDB-Clientbibliotheken.

Überblick

Datenspeicherentitäten weisen einen Schlüssel und einen Satz von Properties auf. Bei einer Anwendung wird die Datastore API verwendet, um Datenmodelle zu definieren und Instanzen dieser Modelle zu erstellen, die als Entitäten gespeichert werden. Modelle bieten eine gemeinsame Struktur für die Entitäten, die von der API erstellt werden. Außerdem können sie verwendet werden, um Regeln zum Validieren von Property-Werten zu definieren.

Modellklassen

Die Modellklasse

Bei Anwendungen werden die verschiedenen verwendeten Datenarten mit Modellen beschrieben. Ein Modell ist eine Python-Klasse, die die Attribute der Klasse Modell übernimmt. Mit der Modellklasse werden eine neue Art von Datenspeicherentität sowie die Properties definiert, die die Art erwartungsgemäß aufweist. Der Name der Art wird durch den instanziierten Klassennamen bestimmt, der von db.Model übernommen wird.

Modell-Properties werden mit Klassenattributen in Bezug auf die Modellklasse definiert. Jedes Klassenattribut ist eine Instanz einer abgeleiteten Klasse der Property-Klasse, bei der es sich normalerweise um eine der bereitgestellten Property-Klassen handelt. Eine Property-Instanz umfasst die Konfiguration der Property. Dazu zählen zum Beispiel die Entscheidung, ob die Property für die Gültigkeit der Instanz erforderlich ist oder einen Standardwert darstellt, den die Instanz verwendet, wenn kein Wert angegeben wird.

from google.appengine.ext import db

class Pet(db.Model):
    name = db.StringProperty(required=True)
    type = db.StringProperty(required=True, choices=set(["cat", "dog", "bird"]))
    birthdate = db.DateProperty()
    weight_in_pounds = db.IntegerProperty()
    spayed_or_neutered = db.BooleanProperty()

In der API werden Entitäten einer der definierten Entitätenarten von der Instanz der entsprechenden Modellklasse dargestellt. Durch Aufrufen des Konstruktors der Klasse können in der Anwendung neue Entitäten erstellt werden. Die Anwendung verwendet Attribute der Instanz, um auf Properties der Entität zuzugreifen und diese zu verändern. Beim Modellinstanzkonstruktor sind anfängliche Werte für Properties als Schlüsselwortargumente zulässig.

from google.appengine.api import users

pet = Pet(name="Fluffy",
          type="cat")
pet.weight_in_pounds = 24

Hinweis: Die Attribute der Modellklasse stellen die Konfiguration der Modell-Properties dar, deren Werte Property-Instanzen sind. Die Attribute der Modellinstanz sind die eigentlichen Property-Werte, die als Typ vorliegen, der von der Property-Klasse akzeptiert wird.

Bei der Modellklasse werden die Property-Instanzen zum Validieren von Werten verwendet, die den Modellinstanzattributen zugeordnet sind. Die Validierung von Property-Werten wird durchgeführt, wenn eine Modellinstanz zum ersten Mal erstellt und wenn einem Instanzattribut ein neuer Wert zugeordnet wird. Dadurch wird sichergestellt, dass eine Property niemals einen ungültigen Wert aufweist.

Da die Validierung beim Erstellen der Instanz durchgeführt wird, muss jede Property, die als erforderlich konfiguriert ist, im Konstruktor initialisiert werden. In diesem Beispiel sind name und type erforderliche Werte. Daher werden ihre Anfangswerte im Konstruktor angegeben. weight_in_pounds ist im Modell nicht erforderlich, sodass anfangs keine Zuordnung erfolgt und erst später ein Wert zugeordnet wird.

Eine Instanz eines Modells, das mit dem Konstruktor erstellt wurde, ist erst im Datenspeicher vorhanden, wenn es zum ersten Mal „eingefügt“ wird.

Hinweis: Wie bei allen Python-Klassenattributen wird die Modell-Property-Konfiguration beim ersten Importieren des Skripts oder Moduls initialisiert. Da in App Engine importierte Module zwischen Requests im Cache gespeichert werden, kann die Modulkonfiguration während des Requests für einen Nutzer initialisiert und während des Requests für einen anderen Nutzer wiederverwendet werden. Initialisieren Sie keine Modell-Property-Konfiguration, wie Standardwerte, mit spezifischen Daten für den Request oder den aktuellen Nutzer. Weitere Informationen finden Sie unter Anwendungs-Caching.

Die Expando-Klasse

Über ein mit der Modellklasse definiertes Modell wird ein fester Satz von Properties erstellt, den jede Instanz der Klasse haben muss, eventuell mit Standardwerten. Dies ist eine nützliche Methode zum Modellieren von Datenobjekten. Für den Datenspeicher ist es jedoch nicht erforderlich, dass jede Entität eines bestimmten Typs den gleichen Satz von Properties aufweist.

In bestimmten Fällen ist es nützlich, wenn Entitäten Properties aufweisen, die nicht unbedingt den Properties anderer Entitäten des gleichen Typs entsprechen. Eine solche Entität wird in der Datastore API durch ein Expando-Modell dargestellt. Eine Expando-Modellklasse ist die abgeleitete Klasse einer Expando-Basisklasse. Jeder Wert, der einem Attribut einer Expando-Modellinstanz zugeordnet wird, wird zu einer Property der Datenspeicherentität und erhält den Namen des Attributs. Diese Properties werden als dynamische Properties bezeichnet. Mit Property-Klasse-Instanzen definierte Properties in Klassenattributen sind feste Properties.

Expando-Modelle können sowohl feste als auch dynamische Properties aufweisen. Mit der Modellklasse werden einfach nur Klassenattribute mit Property-Konfigurationsobjekten für die festen Properties festgelegt. Beim Zuordnen von Werten zu dynamischen Properties werden diese dynamischen Properties in der Anwendung erstellt.

class Person(db.Expando):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    hobbies = db.StringListProperty()

p = Person(first_name="Albert", last_name="Johnson")
p.hobbies = ["chess", "travel"]

p.chess_elo_rating = 1350

p.travel_countries_visited = ["Spain", "Italy", "USA", "Brazil"]
p.travel_trip_count = 13

Da dynamische Properties keine Modell-Property-Definitionen haben, werden dynamische Properties nicht validiert. Jede dynamische Property kann einen beliebigen Wert der Datenspeicher-Basistypen haben, zum Beispiel None. Zwei Entitäten des gleichen Typs können unterschiedliche Werttypen für die gleiche dynamische Property aufweisen. So kann bei einer Entität eine Property nicht festgelegt sein, die bei der anderen Property festgelegt wurde.

Im Gegensatz zu festen Properties müssen dynamische Properties nicht vorhanden sein. Eine dynamische Property mit dem Wert None unterscheidet sich von einer nicht vorhandenen Property. Für den Fall, dass eine Expando-Modellinstanz kein Attribut für eine Property aufweist, weist die entsprechende Datenentität diese Property nicht auf. Dynamische Properties werden durch Löschen des Attributs gelöscht.

Attribute, deren Namen mit einem Unterstrich (_) beginnen, werden nicht in der Datenspeicherentität gespeichert. Dadurch können Sie Werte in der Modellinstanz zur vorübergehenden, internen Verwendung speichern, ohne dass sich dies auf die Daten auswirkt, die mit der Entität gespeichert werden.

Hinweis: Statische Properties werden immer in der Datenspeicherentität gespeichert, unabhängig davon, ob es sich um die Expando- oder Modellklasse handelt oder ob sie mit einem Unterstrich (_) beginnen.

del p.chess_elo_rating

Abfragen, bei denen eine dynamische Property in einem Filter verwendet wird, geben nur Entitäten zurück, deren Wert für die Property den gleichen Typ aufweist wie der Wert, der in der Abfrage verwendet wird. Analog dazu gibt die Abfrage nur Entitäten zurück, bei denen diese Property festgelegt ist.

p1 = Person()
p1.favorite = 42
p1.put()

p2 = Person()
p2.favorite = "blue"
p2.put()

p3 = Person()
p3.put()

people = db.GqlQuery("SELECT * FROM Person WHERE favorite < :1", 50)
# people has p1, but not p2 or p3

people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1", 50)
# people has no results

Hinweis: Im obigen Beispiel werden Abfragen für Entitätengruppen verwendet, die möglicherweise veraltete Ergebnisse zurückgeben. Innerhalb von Entitätengruppen sollten Sie Ancestor-Abfragen verwenden, um strikt konsistente Ergebnisse zu erhalten.

Die Expando-Klasse ist eine abgeleitete Klasse der Model-Klasse und übernimmt alle ihre Methoden.

Die PolyModel-Klasse

Die Python API enthält eine weitere Klasse für die Datenmodellierung, mit der Klassenhierarchien definiert und Abfragen durchgeführt werden können. Mit diesen Abfragen können Entitäten einer bestimmten Klasse oder einer abgeleiteten Klasse zurückgegeben werden. Modelle und Abfragen dieser Art werden als „polymorph“ bezeichnet, weil sie es ermöglichen, dass Instanzen einer Klasse Ergebnisse für eine Abfrage einer übergeordneten Klasse sind.

Im folgenden Beispiel wird eine Contact-Klasse mit den abgeleiteten Klassen Person und Company definiert:

from google.appengine.ext import db
from google.appengine.ext.db import polymodel

class Contact(polymodel.PolyModel):
    phone_number = db.PhoneNumberProperty()
    address = db.PostalAddressProperty()

class Person(Contact):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    mobile_number = db.PhoneNumberProperty()

class Company(Contact):
    name = db.StringProperty()
    fax_number = db.PhoneNumberProperty()

Mit diesem Modell wird sichergestellt, dass alle Person- und Company-Entitäten die Properties phone_number und address haben. Außerdem wird sichergestellt, dass bei Anfragen nach Contact-Entitäten Person- oder Company-Entitäten zurückgegeben werden können. Nur Person-Entitäten haben mobile_number-Properties.

Die abgeleiteten Klassen können wie jede andere Modellklasse instanziiert werden:

p = Person(phone_number='1-206-555-9234',
           address='123 First Ave., Seattle, WA, 98101',
           first_name='Alfred',
           last_name='Smith',
           mobile_number='1-206-555-0117')
p.put()

c = Company(phone_number='1-503-555-9123',
            address='P.O. Box 98765, Salem, OR, 97301',
            name='Data Solutions, LLC',
            fax_number='1-503-555-6622')
c.put()

Bei Anfragen nach Contact-Entitäten können Instanzen von Contact, Person oder Company zurückgegeben werden. Mit dem folgenden Code werden die Informationen für beide der oben erstellten Entitäten gedruckt:

for contact in Contact.all():
    print 'Phone: %s\nAddress: %s\n\n' % (contact.phone_number,
                                          contact.address)

Bei Anfragen nach Company-Entitäten werden nur Instanzen von Company zurückgegeben:

for company in Company.all()
    # ...

Derzeit sollten polymorphe Modelle nicht direkt an den Query-Klassen-Konstruktor übergeben werden. Verwenden Sie stattdessen wie im obigen Beispiel die all()-Methode.

Weitere Informationen zur Verwendung polymorpher Modelle und ihrer Implementierung finden Sie unter Die PolyModel-Klasse.

Property-Klassen und Typen

Der Datenspeicher unterstützt einen festen Satz an Werttypen für Entitäts-Properties. Dazu zählen Unicode-Strings, Ganzzahlen, Gleitkommazahlen, Datumsangaben, Entitätsschlüssel, Byte-Strings (Blobs) und verschiedene GData-Typen. Alle Datenspeicherwerttypen haben eine entsprechende Property-Klasse, die vom Modul google.appengine.ext.db bereitgestellt wird.

In Typen und Property-Klassen werden alle unterstützten Werttypen und ihre zugehörigen Property-Klassen erläutert. Im Folgenden werden einige spezielle Werttypen beschrieben.

Strings und Blobs

Der Datenspeicher unterstützt zwei Werttypen zum Speichern von Text: kurze Textstrings mit einer Länge von bis zu 1.500 Byte und lange Textstrings mit einer Länge von bis zu einem Megabyte. Kurze Strings weisen einen Index auf und können in Abfragefilterbedingungen und Sortierfolgen verwendet werden. Lange Strings weisen keinen Index auf und können nicht in Filterbedingungen und Sortierfolgen verwendet werden.

Ein kurzer Stringwert kann entweder den Wert unicode oder den Wert str haben. Beim Wert str wird angenommen, dass die Codierung 'ascii' vorliegt. Um für den Wert str eine andere Codierung anzugeben, können Sie diesen mit dem Typkonstruktor unicode() in den Wert unicode konvertieren. Dabei werden str sowie der Name der Codierung als Argumente verwendet. Kurze Strings können mithilfe der StringProperty-Klasse modelliert werden.

class MyModel(db.Model):
    string = db.StringProperty()

obj = MyModel()

# Python Unicode literal syntax fully describes characters in a text string.
obj.string = u"kittens"

# unicode() converts a byte string to a Unicode string using the named codec.
obj.string = unicode("kittens", "latin-1")

# A byte string is assumed to be text encoded as ASCII (the 'ascii' codec).
obj.string = "kittens"

# Short string properties can be used in query filters.
results = db.GqlQuery("SELECT * FROM MyModel WHERE string = :1", u"kittens")

Ein langer Stringwert wird durch eine db.Text-Instanz dargestellt. Deren Konstruktor nimmt den Wert unicode oder str an. Optional wird auch der Name der Codierung angenommen, der bei str verwendet wird. Lange Strings können mithilfe der TextProperty-Klasse modelliert werden.

class MyModel(db.Model):
    text = db.TextProperty()

obj = MyModel()

# Text() can take a Unicode string.
obj.text = u"lots of kittens"

# Text() can take a byte string and the name of an encoding.
obj.text = db.Text("lots of kittens", "latin-1")

# If no encoding is specified, a byte string is assumed to be ASCII text.
obj.text = "lots of kittens"

# Text properties can store large values.
obj.text = db.Text(open("a_tale_of_two_cities.txt").read(), "utf-8")

Der Datenspeicher unterstützt außerdem zwei ähnliche Typen für Nicht-Text-Byte-Strings: db.ByteString und db.Blob. Bei diesen Werten handelt es sich um Strings aus Rohbyte, die nicht wie UTF-8 als codierter Text gehandhabt werden.

Wie db.StringProperty-Werte haben auch db.ByteString-Werte einen Index. Wie db.TextProperty-Properties sind auch db.ByteString-Werte auf 1.500 Byte beschränkt. Eine ByteString-Instanz stellt einen kurzen Byte-String dar, bei dem ein str-Wert als Argument für deren Konstruktor angenommen wird. Byte-Strings werden mithilfe der ByteStringProperty-Klasse modelliert.

Wie beim Wert db.Text kann ein db.Blob-Wert bis zu einem Megabyte groß sein, hat jedoch keinen Index. Außerdem kann der Wert nicht in Anfragefiltern oder Sortierfolgen verwendet werden. Bei der Klasse db.Blob wird ein str-Wert als Argument für den Konstruktor angenommen. Alternativ können Sie den Wert direkt zuweisen. Blobs werden mithilfe der BlobProperty-Klasse modelliert.

class MyModel(db.Model):
    blob = db.BlobProperty()

obj = MyModel()

obj.blob = open("image.png").read()

Listen

Eine Property kann mehrere Werte haben, die in der Datastore API als Python-list dargestellt werden. Die Liste kann Werte aller Werttypen enthalten, die vom Datenspeicher unterstützt werden. Auch eine einzelne Listen-Property kann Werte verschiedener Typen aufweisen.

Die Reihenfolge wird im Allgemeinen beibehalten. Wenn Entitäten von Abfragen und get() zurückgegeben werden, befinden sich die Werte der Listen-Properties daher in derselben Reihenfolge wie beim Speichern. Es gibt eine Ausnahme: Blob- und Text-Werte werden an das Ende der Liste verschoben, behalten jedoch ihre ursprüngliche Ordnung zueinander bei.

Die ListProperty-Klasse modelliert eine Liste und erzwingt, dass alle Werte in der Liste einen bestimmten Typ haben. Der Einfachheit halber stellt die Bibliothek auch StringListProperty bereit, ähnlich wie ListProperty(basestring).

class MyModel(db.Model):
    numbers = db.ListProperty(long)

obj = MyModel()
obj.numbers = [2, 4, 6, 8, 10]

obj.numbers = ["hello"]  # ERROR: MyModel.numbers must be a list of longs.

Bei einer Abfrage mit Filtern für eine Listen-Property werden alle Werte in der Liste einzeln getestet. Die Entität stimmt nur dann mit der Abfrage überein, wenn ein Wert in der Liste alle Filter dieser Property übergibt. Weitere Informationen finden Sie auf der Seite Datenspeicherabfragen.

# Get all entities where numbers contains a 6.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers = 6")

# Get all entities where numbers contains at least one element less than 10.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers < 10")

Abfragefilter funktionieren nur bei Mitgliedern der Liste. Zwei Listen können mit einem Abfragefilter nicht auf Ähnlichkeiten überprüft werden.

Intern werden im Datenspeicher Listen-Property-Werte als mehrere Werte für die Property dargestellt. Eine Property weist keine Darstellung im Datenspeicher auf, wenn ein Listen-Property-Wert einer leeren Liste entspricht. In der Datastore API wird diese Situation bei statischen Properties (mit ListProperty) und dynamischen Properties unterschiedlich gehandhabt:

  • Einem statischen ListProperty-Modell kann die leere Liste als Wert zugeordnet werden. Die Property ist im Datenspeicher nicht vorhanden. Das Verhalten der Modellinstanz entspricht jedoch einer Situation, bei der der Wert die leere Liste ist. Ein statisches ListProperty-Modell kann keinen None-Wert haben.
  • Einer dynamischen Property mit einem list-Wert kann kein leerer Listenwert zugewiesen werden. Allerdings kann sie den Wert None haben und mit del gelöscht werden.

Das ListProperty-Modell testet, dass ein der Liste hinzugefügter Wert den richtigen Typ aufweist, und löst einen BadValueError aus, wenn dies nicht der Fall ist. Dieser Test erfolgt (und schlägt möglicherweise fehl), selbst wenn eine zuvor gespeicherte Entität abgerufen und in das Modell geladen wird. Da str-Werte vor dem Speichern als ASCII-Text in unicode-Werte konvertiert werden, wird der Wert ListProperty(str) als Wert ListProperty(basestring) gehandhabt. Dabei handelt es sich um den Python-Datentyp, bei dem sowohl der Wert str als auch der Wert unicode akzeptiert werden. Auch StringListProperty() kann zu diesem Zweck verwendet werden.

Verwenden Sie zum Speichern von Nicht-Text-Byte-Strings Werte vom Typ db.Blob. Bytes von Blob-Strings werden beim Speichern und Abrufen beibehalten. Properties, die aus einer Liste mit Blobs bestehen, können als ListProperty(db.Blob) deklariert werden.

Listen-Properties können auf ungewöhnliche Weise mit Sortierfolgen interagieren. Einzelheiten finden Sie auf der Seite Datenspeicherabfragen.

Verweise

Property-Werte können den Schlüssel anderer Entitäten enthalten. Der Wert ist eine Schlüsselinstanz.

Die ReferenceProperty-Klasse modelliert einen Schlüsselwert und erzwingt, dass alle Werte auf Entitäten einer bestimmten Art verweisen. Der Einfachheit halber stellt die Bibliothek auch SelfReferenceProperty bereit, das einer ReferenceProperty entspricht, die auf dieselbe Art verweist wie die Entität mit der Property.

Beim Zuordnen einer Modellinstanz zur Property ReferenceProperty wird deren Schlüssel automatisch als Wert verwendet.

class FirstModel(db.Model):
    prop = db.IntegerProperty()

class SecondModel(db.Model):
    reference = db.ReferenceProperty(FirstModel)

obj1 = FirstModel()
obj1.prop = 42
obj1.put()

obj2 = SecondModel()

# A reference value is the key of another entity.
obj2.reference = obj1.key()

# Assigning a model instance to a property uses the entity's key as the value.
obj2.reference = obj1
obj2.put()

Ein Property-Wert ReferenceProperty kann so verwendet werden, als ob es sich um die Modellinstanz der Entität handelt, auf die verwiesen wird. Wenn sich die Entität, auf die verwiesen wird, nicht im Speicher befindet, wird durch Verwendung der Property als Instanz automatisch die Entität aus dem Datenspeicher abgerufen. Eine ReferenceProperty speichert ebenfalls einen Schlüssel. Die Verwendung der Property führt jedoch dazu, dass die zugehörige Entität geladen wird.

obj2.reference.prop = 999
obj2.reference.put()

results = db.GqlQuery("SELECT * FROM SecondModel")
another_obj = results.fetch(1)[0]
v = another_obj.reference.prop

Wenn ein Schlüssel auf eine nicht vorhandene Entität verweist, wird beim Zugriff auf die Property ein Fehler ausgelöst. Wenn bei einer Anwendung davon ausgegangen wird, dass ein Verweis ungültig sein könnte, kann das Vorhandensein des Objekts mit einem try/except-Block geprüft werden:

try:
  obj1 = obj2.reference
except db.ReferencePropertyResolveError:
  # Referenced entity was deleted or never existed.

ReferenceProperty-Properties weisen eine weitere nutzerfreundliche Funktion auf: Rückverweise. Wenn ein Modell eine ReferenceProperty zu einem anderen Modell hat, erhält jede referenzierte Entität eine Property, deren Wert eine Abfrage ist, die alle Entitäten des ersten Modells zurückgibt, die auf sie verweisen.

# To fetch and iterate over every SecondModel entity that refers to the
# FirstModel instance obj1:
for obj in obj1.secondmodel_set:
    # ...

Der Name der Rückverweis-Property lautet standardmäßig modelname_set (mit dem Namen der Modellklasse in Kleinbuchstaben und „_set“ am Ende) und kann mit dem Argument collection_name an den ReferenzProperty-Konstruktor angepasst werden.

Für den Fall, dass mehrere ReferenceProperty-Werte auf die gleiche Modellklasse verweisen, wird bei der Standardkonstruktion der Rückverweis-Property ein Fehler ausgelöst:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class raises a DuplicatePropertyError with the message
# "Class Firstmodel already has property secondmodel_set"
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel)
    reference_two = db.ReferenceProperty(FirstModel)

Um diesen Fehler zu vermeiden, muss das collection_name-Argument explizit festgelegt werden:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class runs fine
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_one_set")
    reference_two = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_two_set")

Automatisches Referenzieren und Dereferenzieren von Modellinstanzen, Typprüfung und Rückverweise sind nur bei Verwendung der Modell-Property-Klasse ReferenceProperty verfügbar. Schlüssel, die als Werte der dynamischen Expando-Properties oder als ListProperty-Werte gespeichert werden, weisen diese Funktionen nicht auf.