2009年7月6日 星期一

支援交易運算的計數器

支援交易運算


在許多資料庫運算中,為了保持相關的資料庫運算結果能夠不被分割地將同時送進資料庫(也就是一旦有一個操作發生失敗,則整串操作都會同時取消),所以提供了「交易」(transaction)的運算。

Google App Engine 上的 datastore 當然也支援「交易」運算,在上述的例子中,共有兩個資料庫運算--Product entity 的寫入以及 Counter entity 的更新,試著想像這兩個操作若各自(或同時)發生錯誤時會有什麼問題,若是 Product entity 在寫入時發生錯誤,程式就中斷了,那就沒什麼太大的問題,若是 Product entity 成功寫入 datastore,但 Counter entity 在更新時發生錯誤,這時就會造成計數器的統計數量不一致,這時候就適合使用交易運算了。

交易運算的限制


在 Google App Engine 中,交易運算只能操作相同 entity group 的資料,如果我們要將 ProductCounter 的新增動作在同一個交易運算中完成的話,就必須讓它們成為同一個 entity group。

要成為同一個 entity group,不管是 Product 還是 Counter 都需要一個共同的 parent,因此需要一個額外的 data model 來作為這個 entity group 的 root,這裡我提供一個 Index 的 model,順便用來作為每一筆 Product 資料的「序號產生器」 :p
class Index(db.Model):
max_index = db.IntegerProperty(required=True, default=0)

有了這個 model 後,新增 Product 及更新 Counter 的動作就可以改寫為:
# 新增 product 的程式片段...
...
# 從 Index 中取出 Product 的 index
ind = Index.get_by_key_name('product')
if ind is None:
ind = Index()
ind.max_index += 1
ind.put()

# 新增 product entity 並設定 parent 為 ind
p = Product(parent=ind, key_name="product_%d" % ind.max_index,........)
p.put()

# 根據 key_name 取得 counter,並且指定 parent
counter = Counter.get_by_key_name('product_counter', parent=ind.key())
if counter is None:
# 如果 counter 不存在,則建立一個新的,別忘了指定 key_name 及 parent
counter = Counter(parent=ind, key_name='product_counter')

counter.count += 1
counter.put()

在新增 ProductCounter 時,都加上指定 parent 的參數,以此將這些資料建構成一個 entity group,然而,一旦資料在 entity group 中,在取出時也要加上 parent 參數。

作成交易運算的函式


因此,我們可以把上面的程式碼包成一個函式,比方說是 create_product,這樣就可以利用 Datastore API 中的 run_in_transaction 函式來作成交易運算了。

....
def create_product():
# 放入上述的程式碼

....
# 新增資料時...
from google.appengine.ext import db
db.run_in_transaction(create_product)


如此一來,只有當 Index, ProductCounter 的資料操作都成功時,更新的資料才會進入 datastore 中。

2009年6月18日 星期四

資料計數器

Datastore API 的限制


在使用 Datastore API 時有個限制,就是每作一次 query ,最多能存取到的資料數量為 1000 筆(註1),也因為這個限制,所以也不能直接使用 Query 物件下的 count() 方法來計算資料數量。

舉例來說,如果要設計一個網路商店,每一件商品是一個 Product entity,假如網站中的商品數量超過 1000 筆,那這樣的程式碼:
from google.appengine.ext import db

class Product(db.Model):
...
# product 各欄位

...
query = Product.all()
number_of_products = query.count()

並不會幫你統計出究竟有多少 Product entity,因為 query 的結果最多就是 1000 筆資料。

手動記錄


為了解決這樣的問題,其實可以自己定義一個「計數器」的資料,用來統計究竟有多少資料,於是立刻可以寫出這樣的 data model:
from google.appengine.ext import db

class Counter(db.Model):
count = db.IntegerProperty(required=True, default=0)

有了這樣的資料模型,便能夠用來統計網站中任何資料的數量。我們可以經由不同的 key_name 來區分出不同的計數器,根據上述的例子,便可以在新增 Product 時,在計數器上加1以便統計。
# 新增 product 的程式片段...
...
p = Product(........) # 新增 product entity
p.put()

# 根據 key_name 取得 counter
counter = Counter.get_by_key_name('product_counter')
if counter is None:
# 如果 counter 不存在,則建立一個新的,別忘了指定 key_name
counter = Counter(key_name='product_counter')

counter.count += 1
counter.put()

以此類推,當要刪除一個或多個 Product entities 時,也要同步更新對應的計數器資料,以保持資料數量的一致性(consistency)。



  1. 之後會介紹如何讀出超過1000筆的資料

屬性中的 indexed=False

如果你的程式中,有個 data model 的 property 很多,那資料在存取時會隨著 property 數量的成長而降低效能(尤其是發生在呼叫 put() 來儲存資料時),這是因為在儲存或更新資料時,datastore 也會針對這些 property 作一些 index 的動作,使得整個操作的時間變長。

如果確定不會用某個 property 來作資料查詢 filter 的條件,那麼可以在定義 property 時加上 indexed=False 參數,提示 datastore 這個 property 不要做 index,範例程式如下:
from google.appengine.ext import db

class Foo(db.Model):
....
bar = db.StringProperty(indexed=False)

2009年6月15日 星期一

為儲存的內容加上標籤(簡單版)

現在很多網站都會為儲存的內容加上標籤,像 Flickr 的每一張相片都可以設定標籤:



或是像 delicious 的書籤也可以設定標籤:



在 App Engine 上開發應用程式,如果要為儲存的內容加上標籤的支援,那麼在資料模型(Data Model)的定義時,可以加上一個 property:
from google.appengine.ext import db
class MyContent(db.Model):
....
tags = db.StringListProperty()

如此一來,tags就可以儲存由字串所組成的 list資料,也就是可以儲存 MyContent 物件的標籤。

假設使用者透過表單送出標籤的資料,而標籤的資料是一個以逗號(,)隔開的字串,那麼要儲存標籤的作法就是:
....
tags_string = self.request.get('tags')
content = MyContent()
....
content.tags = map(lambda x: x.strip(), tags_string.split(','))
content.put()
....

雖然 tags_string.split(',') 已經產生一個由字串所組成的 list 了,但是使用者輸入的字串,可能會在逗號的前後留下空白,所以用了 map 函數將 list 中的每個元素空白消掉(strip 函數)

如果要取出含有某個標籤值(如:food)的內容,則可以直接使用 GQL 中特殊的語法來取出:
....
query = MyContent.gql('WHERE tags = :1', 'food')
for content in query:
....

因為 GQL 的設計,雖然 tags 欄位是一個 list,但是只要比對的元素有出現在 list 中,則 = 運算的結果就會是 True,所以就能夠輕易地根據 list 中的元素來查詢資料。

本部落格的宗旨

筆者因為深感 Google App Engine 的中文討論資源不足,所以打算把自己的研究整理成專門的 blog 來發表,順便與同好們互相討論學習。

未來這個部落格將會不定期更新一些在 Google App Engine 上撰寫程式的技巧及範例,主要會以 Python 版本為主,若有筆誤或是觀念錯誤的部份,還請各位讀者不吝賜教。