Encje i modele datastore
Modele
Model to klasa Pythona dziedzicząca klasę Model udostępnianą przez API. Każdy atrybut to obiekt klasy dziedziczącej klasę Property i będący właściwością encji. Klasa ta przechowuje informacje o właściwości, a także opcje konfiguracyjne (jak np. wymóg podania wartości, czy też podania wartości o ściśle określonym typie), oto przykład:
from google.appengine.ext import db
# klasa modelu opisująca dany typ encji
class Pet(db.Model):
name = db.StringProperty(required=True)
type = db.StringProperty(required=True, choices=set(["kot", "pies", "ptak"]))
birthdate = db.DateProperty()
weight = db.IntegerProperty()
spayed_or_neutered = db.BooleanProperty()
owner = db.UserProperty(required=True)
from google.appengine.api import users
pet = Pet(name="Koteczek",
type="kot",
owner=users.get_current_user())
pet.weight = 24
Jako że walidacja wartości atrybutów odbywa się przy tworzeniu instancji - należy w konstruktorze podać wartości dla wszystkich wymaganych właściwości. W powyższym przykładzie właściwość "weight" nie jest wymagana i wartość można podać po stworzeniu instancji (lub w ogóle jej nie podawać).
Należy zwrócić uwagę, że GAE keszuje importowane moduły między żądaniami. Konfiguracja modułu może zosta zainicjalizowana dla żądania nadesłanego przez jednego użytkownika i wykorzystana przy żądaniu nadesłanym przez innego. Nie ustawiaj konfiguracji modeli, np. domyślnych wartości zależnych od bieżącego użytkownika.
Modele Expando
Modele definiowane poprzez API określają stałą strukturę danych encji danego typu, jednakże datastore nie wymaga by wszystkie encje danego typu miały taki sam układ właściwości. Czasami potrzeba by jakaś encja miała inne właściwości. Tego typu encje można tworzyć poprzez API za pomocą modeli "expando".
Model expando dziedziczy klasę Expando i może mieć listę właściwości określoną za pomocą atrybutów jak zwykły model. W odróżnieniu od typowego modelu - każda wartość przypisana do atrybutu nie wymienionego w modelu w definicjach właściwości zostanie zapisana jako dynamiczne właściwości. Jako że dynamiczne właściwości nie są opisane - nie są także walidowane. Oto przykład:
class Person(db.Expando):
first_name = db.StringProperty()
last_name = db.StringProperty()
hobbies = db.StringListProperty()
# statyczne właściwości
p = Person(first_name="Albert", last_name="Johnson")
p.hobbies = ["chess", "travel"]
# dynamiczne właściwości obsługiwane przez Expando
p.chess_elo_rating = 1350
p.travel_countries_visited = ["Spain", "Italy", "USA", "Brazil"]
p.travel_trip_count = 13
Zapytanie, które filtruje wyniki po wartości dynamicznej właściwości zwróci tylko te encje, które mają taką właściwość i wartość której jest zgodna co do typu (i wartości) z tą podaną w zapytaniu.
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 ma p1, ale nie p2 czy p3
people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1", 50)
# people nie ma żadnych encji
Modele polimorficzne
Pythonowe API zawiera jeszcze jedną klasę umożliwiającą definiowanie klas modeli zależnych od siebie hierarchią. Zapytanie wykonane na modelu, któremu hierarchicznie podlega kilka innych modeli zwróci w tym przypadku także encje zależnych modeli. Takie modele i zapytania nazywa się polimorficznymi, gdyż zezwalając instancji jednej klasy być wynikami zapytania klasy-rodzica.
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()
Instancje klas-modeli można tworzyć tak (zwróć uwagę na obecność właściwości phone_number i address):
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()
Zapytanie dotyczące encji Contact może zwrócić encje Contact, Person czy Company. Zapytanie dotyczące encji Company zwróci tylko encje Company. Zapytanie na polimorficznej klasie Contact można wykonać tak:
for contact in Contact.all():
print 'Phone: %s
Address: %s
'
% (contact.phone,
contact.address))
Właściwości i typy
Datastore obsługuje szereg typów wartości jakie przyjąć może właściwość encji, a są to łańcuchy unikodu, liczby całkowite, liczby zmiennoprzecinkowe, daty, klucze encji, łańcuchy bajtowe (bloby) i różne typy GData. Każdy z tych typów posiada własną klasę Property dostępną w API w module google.appengine.ext.db.
Datastore obsługuje dwa typy wartości dla łańcuchów: krótkie łańcuchy do 500 bajtów długości i długie łańcuchy do 1 MB długości. Krótkie łańcuchy podlegają indeksom i można operować na nich w zapytaniach. Długie łańcuchy nie podlegają indeksom i nie można filtrować, sortować po nich encji.
Krótki łańcuch może być unikodem (unicode), lub zwykłym łańcuchem (str) z założeniem domyślnego kodowania na ascii. Krótkie łańcuchy w modelu oznacza się jako StringProperty:
class MyModel(db.Model):
string = db.StringProperty()
obj = MyModel()
# unikod
obj.string = u"kittens"
# unicode() konwertujący łańcuch o kodowaniu ascii do unikodu
obj.string = unicode("kittens", "latin-1")
# łańcuch ascii
obj.string = "kittens"
# krótkich łańcuchów można użyć do filtrowania i sortowania encji
results = db.GqlQuery("SELECT * FROM MyModel WHERE string = :1", u"kittens")
Długi łańcuch jest reprezentowany przez instancję db.Text. Konstruktor przyjmuje jako wartość unikod lub zwykły łańcuch z opcjonalnym podaniem kodowania. Długie łańcuchy w modelach określane są przez TextProperty.
class MyModel(db.Model):
text = db.TextProperty()
obj = MyModel()
# unikod
obj.text = db.Text(u"lots of kittens")
# zwykły łańcuch z podanym kodowaniem
obj.text = db.Text("lots of kittens", "latin-1")
# domyślnie zakładane kodowanie ascii
obj.text = db.Text("lots of kittens")
# db.Text może przechowywać bardzo długie łańcuchy
obj.text = db.Text(open("a_tale_of_two_cities.txt").read(), "utf-8")
Datastore obsługuje także dwa podobne typy dla łańcuchów bajtowych - db.ByteString i db.Blob. Te łańcuchy składające się z surowych bajtów nie są traktowane jako unikod (np. utf-8). ByteString ma limit na 500 znaków i podlega indeksom bazy (ByteStringProperty w modelach), a Blob może przechowywać dłuże łańcuchy, lecz nie podlega indeksom (BlobProperty w modelach). Typy te są stosowane do przechowywania binarnych danych - np. grafiki:
class MyModel(db.Model):
blob = db.BlobProperty()
obj = MyModel()
obj.blob = db.Blob(open("image.png").read())
Pythonowe listy służą do przedstawiania właściwości mającej wiele wartości. Listy mogą zawierać dowolne wartości o obsługiwanych przez datastore typach. Zapisywana jest także kolejność elementów na liście. Klasa ListProperty służy do opisania takiej listy i może wymusić by wszystkie jej wartości były określonego typu. Dostępna jest także pomocnicza klasa StringListProperty dla listy łańcuchów. Oto przykład:
class MyModel(db.Model):
numbers = db.ListProperty(long)
obj = MyModel()
obj.numbers = [2, 4, 6, 8, 10]
obj.numbers = ["hello"] # tutaj będzie błąd bo lista musi mieć tylko longi
Zapytanie na właściwość mającą jako wartość listę będzie sprawdzało każdy jej element i zostanie spełnione jeżeli co najmniej jeden element listy spełnia warunek zapytania
# 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")
Do statycznej ListProperty można przypisać pustą listę, lecz dla datastore w efekcie będzie to encja bez danej właściwości. Statyczna ListProperty nie może mieć None jako wartości. Dynamiczna lista nie może mieć przypisanej pustej listy, lecz może mieć wartość None.
Do przechowywania nie-tekstowych danych używaj db.Blob. Do stworzenia listy takich binarnych danych można użyć "ListProperty(db.Blob)."
Właściwość może przyjąć jako wartość klucz innej encji. Wartością jest tutaj instancja klucza. Referencja tego typu opisywana jest przez ReferenceProperty i wymusza by wartości odnosiły się do kluczy encji danego typu. Dla uproszczenia dodano także SelfReferenceProperty, która dopuszcza tylko instancje kluczy encji tego samego modelu. Przypisując jako referencję instancję klasy spowoduje automatyczne użycie jej klucza:
class FirstModel(db.Model):
prop = db.IntegerProperty()
class SecondModel(db.Model):
reference = db.ReferenceProperty(FirstModel)
obj1 = FirstModel()
obj1.prop = 42
obj1.put()
obj2 = SecondModel()
# atrybut reference przyjmuje jako wartość instancję klucza innej encji
obj2.reference = obj1.key()
# przypisanie instancji encji także spowoduje przypisanie instancji jej klucza
obj2.reference = obj1
obj2.put()
Gdy encja, które instancja klucza jest gdzieś zapisana jako referencja zostanie skasowana nie spowoduje to zmian w tych referencjach. By sprawdzić czy encja, której instancję klucza mamy zapisaną nie została skasowana można użyć:
obj1 = obj2.reference
if not obj1:
# encja została skasowana
Właściwość ReferenceProperty daje jeszcze jedną przydatną funkcjonalność. Każda encja, do której istnieją referencje dostaje właściwość, której wartością jest zapytanie pobierające wszystkie referujące do niej encje:
# obj1 to instancja encji z modelu FirstModel. Poniższy kod pobierze wszystkie encje z modelu SecondModel
# które mają zapisaną referencję do encji obj1
for obj in obj1.secondmodel_set:
# ...
Jeżeli masz wiele ReferenceProperty do tego samego modelu domyślna konstrukcja wstecznej referencji opisanej powyżej nie będzie funkcjonować. Należy podać określoną nazwę dla każdej wstecznej zależności w referującym modelu poprzez argument collection_name dla ReferenceProperty.
class FirstModel(db.Model):
prop = db.IntegerProperty()
# Tak jest poprawnie
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")
Nazwy właściwości
Datastore rezerwuje wszystkie nazwy właściwości zaczynające się i kończące dwoma podkreśleniami. Aplikacja nie może tworzyć właściwości o takich nazwach.
W Pythonowym API atrybuty w instancjach modeli, których nazwy zaczynają się jednym podkreśleniem są ignorowane i nie są zapisywane do datastore. Można to wykorzystać do tymczasowego przetrzymywania jakiś danych wraz z encją.
Dodatkowo istnieje lista zakazanych nazw właściwości, choć samo datastore zezwala na ich użycie. W Pythonowym API należy podać zakazaną nazwę w argumencie "name" właściwości:
class MyModel(db.Model):
obj_key = db.StringProperty(name="key")
Comment article