Python 的資料建模

附註:強烈建議建構新應用程式的開發人員使用 NDB 用戶端程式庫;相較於此用戶端程式庫,NDB 用戶端程式庫有幾項優點,例如能透過 Memcache API 自動快取實體。如果您目前使用的是舊版 DB 用戶端程式庫,請參閱 DB 至 NDB 遷移指南

總覽

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

模型類別

模型類別

應用程式描述了其與「模型」一起使用的資料種類。模型屬於 Python 類別,且該類別繼承自 Model 類別。模型類別定義一種新的資料儲存庫實體,以及 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 屬性 (property),且 Contact 實體的查詢可以傳回 PersonCompany 實體。僅有 Person 實體擁有 mobile_number 屬性 (property)。

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

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 實體的查詢可以傳回 ContactPerson或是 Company 的執行個體。針對上述建立之兩個實體,以下的程式碼會顯示出其相關資訊:

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 類型。每個資料儲存庫值的類型會有一個相對應的 Property 類別,該類別由 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.ByteString 以及 db.Blob。這些值為原始位元組的字串,且不會視為編碼的文字 (例如 UTF-8)。

db.StringProperty 類似,db.ByteString 值已編入索引。和 db.TextProperty 類似,db.ByteString 值限制為 1500 位元組。ByteString執行個體代表一個位元組的短字串,且提取 str 值做為它建構函式的引數。使用 ByteStringProperty 類別對位元組字串建模。

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

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

obj = MyModel()

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

清單

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

通常順序會獲得保留,所以當查詢和 get()傳回實體,列表之實體值的順序會與他們在儲存時的順序相同。有一個例外情況: BlobText值會移動至列表的末端,但是他們維持彼此相關的原始順序。

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

列表上帶有有篩選器的查詢會單獨測試列表中每一個值。僅有在列表中某些值傳送該屬性上的「所有」篩選器時,實體才會與查詢相符。詳情請參閱資料儲存庫查詢頁面。

# 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),其為一種可以接受 str 以及 unicode 值的 Python 資料類型。您亦可以使用 StringListProperty() 來達到此目的。

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

列表屬性能夠以特殊的方式與排序順序互動,詳情請參閱資料儲存庫查詢頁面。

參考資料

屬性值可以包含其他實體的金鑰。該值為 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 值的金鑰則沒有這些功能。

本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Python 2 適用的 App Engine 標準環境