Python 的資料建模

附註:我們強烈建議建構新應用程式的開發人員使用 NDB 用戶端程式庫,因為 NDB 用戶端程式庫與本用戶端程式庫相較之下有幾個優點,例如能透過 Memcache API 自動將實體加入快取。如果您目前使用的是舊版的 DB 用戶端程式庫,請參閱從 DB 至 NDB 的遷移指南

總覽

資料儲存庫的實體具有一個金鑰以及一組屬性。應用程式使用資料儲存庫 API 定義資料模型,並為要儲存為實體的模型建立執行個體。模型為 API 建立的實體提供通用結構,並可以定義用於驗證屬性值的規則。

模型類別

模型類別

應用程式描述了其與「模型」一起使用的資料種類。模型是繼承自 Model 類別的 Python 類別。模型類別定義一種新的資料儲存庫實體,以及 Kind 預期採用的屬性。Kind 名稱是由繼承自 db.Model 的實例化類別名稱所定義。

使用模型類別上的類別屬性定義 Model 屬性。每個類別屬性都是 Property 類別之子類別的執行個體,通常是其中一個提供的屬性類別。屬性執行個體會保存屬性的設定,例如執行個體是否需要該屬性,或者若沒有提供執行個體,則使用執行個體的預設值。

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()

在 API 中,會由相對應的模型類別來表示所定義的實體種類中的實體。應用程式可以透過呼叫類別的建構函式建立一個新的實體。應用程式使用執行個體的屬性存取與操作實體的屬性。模型執行個體的建構函式接受屬性的初始值做為關鍵字引數。

from google.appengine.api import users

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

注意:模型「類別」的屬性皆為模型屬性的設定,其值皆為 Property 例項。模型「例項」的屬性是實際的屬性值,其值的類型為 Property 類別接受的類型。

Model 類別使用 Property 執行個體,驗證分配給模型執行個體屬性的值。當首次建構模型執行個體,且該執行個體屬性受分配到一個新的值的時候,Property 的值會進行驗證。這可確保屬性永遠不會有無效的值。

由於在建構執行個體時會進行驗證,所以必須在建構函式中初始化任何設定為必要的屬性。在本例中,nametype 是必要值,因此其初始值會在建構函式中指定。模型不需要 weight_in_pounds,因此一開始不會指派,而是稍後才會指派值。

在第一次「put」之前,資料儲存庫中不存在使用建構函式建立的模型執行個體。

附註:與所有 Python 類別屬性相同,當首次匯入指令碼或模組時,會初始化模型屬性的設定。由於 App Engine 會在要求之間快取匯入的模組,因此可以在一個使用者要求期間初始化模組的設定,並且在另一個使用者要求期間重新使用模組設定。不要為特定於要求或當前使用者的資料初始化模型屬性的設定,例如預設值。詳情請參閱「應用程式快取」。

Expando 類別

使用 Model 類別建立的一組固定屬性定義模型,且每個類別的執行個體必須擁有這些屬性 (可能具有預設值)。這是對資料物件建模的好方法,但是資料儲存庫不要求每個給定種類的實體都要擁有相同的一組屬性。

有時候對實體而言,具有一些不需要的屬性亦是有幫助的,例如那些相同種類之其他實體的屬性。這樣的實體會在資料儲存庫 API 中以「expando」模型顯示。expando 模型類別為 Expando 的子類別。任何受分配至 expando 模型之執行個體屬性的值皆會變成資料儲存庫實體的屬性,且以屬性的名稱命名。這些屬性稱為「動態屬性」。在類別屬性中使用 Property 類別例項定義的屬性,稱為固定屬性

expando 模型可以同時擁有固定及動態屬性。模型類別只使用固定屬性的 Property 設定物件來設定類別屬性。當應用程式將值分配給動態屬性時,應用程式即會建立動態屬性。

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

由於動態屬性沒有模型屬性定義,因此動態屬性不會經過驗證。動態屬性皆擁有任何資料儲存庫基本類型的值,包含 None。如果兩個實體是相同類型,則兩者的同一動態屬性可以有不同型別的值,而且其中一個實體可以取消設定另一個實體所設定的屬性。

與固定屬性不同,動態屬性並非必要。值為 None 的動態屬性與不存在的動態屬性不同。若 expando 模型執行個體沒有屬性 (property) 的屬性 (attribute),則相對應的資料實體不具有該屬性 (property)。您可以透過刪除屬性 (attribute) 來刪除動態屬性 (property)。

以底線 (_) 做為開頭的屬性 (attribute)不會儲存至資料儲存庫實體。這可讓您將值儲存在模型執行個體上,暫時供內部使用,而不會影響實體儲存的資料。

注意:「靜態」屬性一律儲存在資料儲存庫實體,無論是 Expando、Model 或是以 (_) 為開頭的屬性。

del p.chess_elo_rating

在篩選器中使用動態屬性的查詢,僅會傳回其屬性值與查詢使用的值為相同型態的實體。同樣地,查詢僅會傳回含有該屬性集合的實體。

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

附註:上述範例以跨實體群組方式使用查詢,這可能會傳回過時的結果。如要取得同步一致的結果,請在實體群組中使用祖系查詢

Expando 類別為 Model 類別的子類別,並沿用其原有的所有方法。

PolyModel 類別

Python API 包含資料模型的另一個類別,可讓您定義類別的階層,並執行可以傳回指定類別或其任何子類別實體的查詢。此類模型和查詢稱為「多型態」,因為其可讓一個類別的執行個體做為父系類別的查詢結果。

以下範例會定義 Contact 類別,其中包含子類別 PersonCompany

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()

這個模型可確保所有 Person 實體和所有 Company 實體都具有 phone_numberaddress 屬性,且 Contact 實體的查詢可傳回 PersonCompany 實體。只有 Person 實體才有 mobile_number 屬性。

子類別可以像其他模型類別一樣實例化:

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()

針對 Contact 實體的查詢可傳回 ContactPersonCompany 的例項。針對上述建立之兩個實體,以下的程式碼會顯示出其相關資訊:

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

針對 Company 實體的查詢只會傳回 Company 的例項:

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

目前,不應將多型態模型直接傳遞至 Query 類別建構函式。請改用 all() 方法,如上述範例所示。

如要進一步瞭解使用多型態模型,以及如何實作,請參閱 PolyModel 類別

Property 類別和類型

資料儲存庫支援實體屬性的一組固定的值類型,包含編碼字串、整數、浮點數、日期、實體金鑰、位元組字串 (blob),以及各種 GData 類型。每個資料儲存庫值類型都有對應的屬性類別,由 google.appengine.ext.db 模組提供。

Types 與 Property 類別描述所有支援之值類型及其相對應之 Property 類別。一些特別的值類型描述如下。

字串和 Blob

資料儲存庫支援兩個用來儲存文字、短文字字串 (長度上限為 1500 位元組)、以及長文字字串 (長度上限為的一兆位元) 之值的類型。短字串會編製為索引,字串可用於查詢篩選條件與排序順序。長字串並未建立索引,因此無法用於篩選條件或排序順序。

短字串值可以是 unicode 值或 str 值。如果值為 str,系統會假設編碼為 'ascii'。如要為 str 值指定不同的編碼,您可以使用 unicode() 類型建構函式將其轉換為 unicode 值,該函式會將 str 和編碼名稱做為引數。您可以使用 StringProperty 類別模擬短字串。

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")

長字串值可以由 db.Text 例項表示。其建構函式會採用 unicode 值,或 str 值,並可選擇採用 str 中使用的編碼名稱。您可以使用 TextProperty 類別模擬長字串。

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")

資料儲存庫亦支援兩種類似型態的非文字位元組字串:db.ByteStringdb.Blob。這些值為原始位元組的字串,且不會視為編碼的文字 (例如 UTF-8)。

db.ByteString 值與 db.StringProperty 值一樣,都會編入索引。與 db.TextProperty 屬性一樣,db.ByteString 值上限為 1500 位元組。ByteString 例項代表位元組的短字串,並將 str 值做為其建構函式的引數。位元組字串的建模方式是使用 ByteStringProperty 類別。

如同 db.Textdb.Blob 值可以大至一兆位元組,但亦不會編入索引,也不能用於查詢篩選器或排序順序。db.Blob 類別會將 str 值做為其建構函式的引數,或者您可以直接指派值。Blob 會使用 BlobProperty 類別建模。

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

obj = MyModel()

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

清單

屬性可擁有多個值,在資料儲存庫 API 中以 Python list 表示。該列表可以包含資料儲存庫所支援之任何類型的值。單一列表屬性甚至可能具有不同類型的值。

通常順序會獲得保留,所以當查詢和 get()傳回實體,列表之實體值的順序會與他們在儲存時的順序相同。但有一個例外狀況:BlobText 值會移至清單的結尾,但彼此之間仍會保留原始順序。

ListProperty類別將列表建模,並強制列表中的所有值都是給定的類型。為了方便起見,該函式庫也提供了類似於 ListProperty(basestring)StringListProperty

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.

列表上帶有有篩選器的查詢會單獨測試列表中每一個值。只有在清單中的某些值通過該屬性的「所有」篩選器時,實體才會與查詢相符。詳情請參閱「Datastore 查詢」頁面。

# 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")

查詢篩選條件的操作僅適用於列表的成員。您無法在一個查詢篩選條件中測試兩個列表的相似性。

在內部,資料儲存庫將列表屬性值表示為屬性的多個值。若列表屬性值是一個空列表,那麼該屬性在資料儲存庫中則沒有內容可以呈現。對於靜態屬性 (使用 ListProperty) 以及動態屬性,資料儲存庫 API 會以不同的方式處理:

  • 靜態的 ListProperty 可以將空列表指派為值。資料儲存庫不存在屬性,但是模型執行個體的行為就如同值為空列表一般。靜態 ListProperty「不能」擁有 None 的值。
  • 具有 list 值的動態屬性「不能」指派空的清單值。不過,其值可以是 None,且可以刪除 (使用 del)。

ListProperty 模型會測試添加至列表的值是否為正確類型,若不是,則會出現 BadValueError。即使之前儲存的實體已獲得擷取並載入至模型中,亦會發生此測試 (且可能會失敗)。由於 str 值會在儲存前轉換為 unicode 值 (以 ASCII 文字形式),因此 ListProperty(str) 會視為 ListProperty(basestring),也就是同時接受 strunicode 值的 Python 資料類型。您也可以使用 StringListProperty() 來達成這個目的。

若要儲存非文字位元組的字串,請使用 db.Blob 值。blob 字串的字元組會在儲存及擷取時保存。您可以將一個 blob 列表的屬性宣告為 ListProperty(db.Blob)

清單屬性可能會以非預期的方式與排序順序互動,詳情請參閱「Datastore 查詢」頁面。

參考資料

屬性值可以包含其他實體的金鑰。該值為 Key 執行個體。

ReferenceProperty 類別為鍵/值建模,並強制所有值參照給定種類的實體。為了方便起見,該函式庫也提供了 SelfReferenceProperty,相當於 ReferenceProperty,可參照具有該屬性的實體。

指派模型執行個體至 ReferenceProperty 屬性會自動使用其金鑰做為值。

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()

您可以把 ReferenceProperty 屬性值當成參照實體的模型執行個體一樣使用。若參照的實體不在記憶體中,則使用該屬性做為執行個體時會自動從資料儲存庫獲取實體。ReferenceProperty 也會儲存金鑰,不過使用此屬性會導致系統載入相關實體。

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

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

如果金鑰指向的實體不存在,則存取此屬性會引發錯誤。如果應用程式預期參照可能無效,可以使用 try/except 區塊測試物件的存在性:

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

ReferenceProperty 還有另一個方便的功能:反向參照。當模型具有 ReferenceProperty 參照至另一個模型時,每個參照實體會取得一個值為 Query 的屬性,該屬性會傳回其引用之第一個模型的所有實體。

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

回溯參照屬性的名稱預設為 modelname_set (模型類別名稱以小寫字母表示,並在結尾加上「_set」),您可以使用 ReferenceProperty 建構函式的 collection_name 引數調整名稱。

如果您有多個 ReferenceProperty 值參照相同的模型類別,則反向參照屬性的預設建構函式會引發錯誤:

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)

如要避免發生這項錯誤,您必須明確設定 collection_name 引數:

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")

模型執行個體、類型確認以及反向參照的自動參照與解除參照,僅可透過ReferenceProperty 模型屬性類別達成。儲存為 Expando 動態屬性或 ListProperty 值的金鑰則沒有這些功能。