2014年10月20日 星期一

GAE 的資料儲存 (Datastore)

由於 HTTP 是無狀態的協定, 每一個 REQUEST 都是彼此獨立, 但一個應用必須通常必須好幾個 REQUEST-RESPONSE 程序才能完成, 因此後端網頁應用程式通常需要將資料處理的結果儲存起來, 以備後續要求使用, 傳統網頁應用伺服器採用關聯式資料庫來處理, 而在 Google 的 GAE 雲端應用則是採用 Datastore 來儲存資料. 由於其技術跟一般傳統的關聯式資料庫差異極大, 因此 Google 稱其為 "資料儲存" (Datastore), 而不稱為資料庫.

GAE 的雲端資料儲存具有下列兩種特性 :
  1. 擴展性 (Scalable) : 可自動管理使用容量, 具有極大彈性
  2. 強固性 (Robust) : 資料儲存於多個位置
Datastore 使用抽象化資料模型, 使用方式需要重新學習, 只有查詢資料儲存的 GQL 語法部分與傳統 SQL 有重疊. 架構上原先採用 Master/Slave, 但因為在定期維護期間資料無法更新, 因此 2011 年以後改推 HRD (High Replication Datastore) 架構, 不僅維護期間仍可使用, 且大幅提升服務穩定性.

學習如何使用 Datastore 前必須了解幾個專門術語 :

  1. 實體 (Entity) :
    Datastore 事實上是一個物件資料庫, 物件的屬性直接對應到資料欄位. 每一個儲存在 Datastore 中的物件稱為 Entity (資料實體). 相當於關聯式資料庫中的列 (Rows) 或紀錄 (Records).
  2. 鍵 (key) :
    Datastore 中的每一個資料實體都具有一個 key, 這在整個系統中是獨一無二的, 用來辨識每一個 Datastore 中的實體. 在 Datastore Python API 中, key 由 db 函式庫中的 Key 類別的實體負責管理. 資料實體 Entity 也提供 key() 與 getkey() 兩個方法來 注意, Key 並非實體的屬性 (property), 因為一個實體一旦建立後其 key 值就固定不能改變, 而屬性值是可以改變的. Key 由下列幾個部份組成 :
    Application ID :
    用來區別此實體屬於哪一個應用程式, 避免別的應用存取它. 透過 key 可以快速地從整個 Datastore 中擷取該資料物件.
    Entity ID/Key name :
    Entity ID 用來辨識實體, 可以在應用程式中指定 (字串), 這時稱為 Key name (使用 key_name 參數), 程式不指定時由 Datastore 自動指派 (數字), 稱為 Entity ID, 但兩者不會同時存在.
    Kind :
    Kind (資料類型) 就是資料類別的名稱, 也用來辨識資料實體 (因為同名實體可能來自不同類型), 相當於關聯式資料庫中的資料表 (Table), 但不同的是, 一個實體的 Kind 與其屬性沒有關係, 因為屬於同一 Kind 的兩個不同資料實體可以有不同的屬性集合, 而且同名屬性的值可以是不同資料類型. 而關聯式資料庫的資料表與其欄位是相關的, 而且資料表內每一個紀錄列, 其欄位屬性與資料類型都須一致. 
  3. 屬性 (property) :
    Property 就是資料實體的欄位, 每一個欄位有其資料類型, 相當於關連式資料庫中資料表的欄位 (Field) 一樣, 不同的是, 實體的欄位允許多值 (multiple values). 注意, 資料實體的Property 對應到物件的 Attribute, 雖然中文均為屬性, 但用的地方不同. 

GAE 提供了操作 Datastore 的 Python API, 其中 Python 物件與 Datastore 之間有直接的對應 :

  1. Python 物件=Datastore 的 Entity  (實體)
  2. Python 物件的屬性 attribute=Datastore 的 Eintity 的 property (屬性)
  3. Python 資料類別 (Class)=Entity 的 Kind (資料類型)


Python Datastore API 詳見 :

https://developers.google.com/appengine/docs/python/datastore/

GAE 的資料儲存類別放在 google.appengine.ext 路徑下的 db 套件, 因此要在程式中使用資料儲存必須先匯入 db 套件 :

from google.appengine.ext import db

在 Datastore 中建立資料實體的方法有兩種 :
  1. 繼承 db.Model 類別
  2. 繼承 db.Expando 類別
這兩者的差別是, 使用 Model 類別須先在類別定義中指定每一個屬性, 因此據此產生的每一個實體都必須符合所定義之結構與資料型態, 如果指定了一個不存在的屬性值或屬性的資料型態不符, 將出現執行時期錯誤; 而繼承 Expando 類別則只要建立一個空白的子類別即可 (用 pass 帶過), 不須在資料類別中事先定義具有哪些成員變數 (屬性), 而是在建立實體 Entity 時, 視資料物件指定了哪些屬性而自動在 Datastore 中建立屬性並賦值, 因此具有擴充性與彈性.

接下來我將在之前的兩篇舊作基礎上進行 Datastore 功能測試, 參考 :

# 如何在 GAE 上佈署 jQuery 與 ExtJS 專案
# 如何在 GAE 中處理 HTTP 要求

下面範例 1 將以建立一個可調整的 jQuery UI 頁籤面板為目標, 順便來測試 Datastore 的功能. 我的構想是在 Datastore 中建立各頁籤的資訊, 當點選頁籤時, 就帶入該頁籤內容, 同時我們可以新增, 刪除, 或修改頁籤設定.

首先用 db.Model 來做, 頁籤 Entity 的屬性定義如下 :

tab_name : 頁籤名稱, 唯一識別用, 英數字串
tab_label : 頁籤顯示文字, 文字
tab_link : 頁籤超連結, 文字
tab_level : 頁籤層級, 1=使用者, 9=管理者, 整數
tab_tip : 頁籤提示語, 文字
tab_order : 頁籤顯示順序, 整數

依據這需求撰寫 tabs.py 程式來定義一個資料類別 Tabs 如下 :

# -*- coding: utf-8 -*-
from google.appengine.ext import db

class Tabs(db.Model):
    tab_name=db.StringProperty()
    tab_label=db.StringProperty()
    tab_link=db.StringProperty()
    tab_level=db.IntegerProperty()
    tab_tip=db.StringProperty()
    tab_order=db.IntegerProperty()

tab1=Tabs()
tab1.tab_name="admin"
tab1.tab_label=u"管理"
tab1.tab_link="/tabtest_admin"
tab1.tab_level=9
tab1.tab_tip=u"系統管理"
tab1.tab_order=99
tab1.put()

tab2=Tabs()
tab2.tab_name="logout"
tab2.tab_label=u"登出"
tab2.tab_link="/tab_logout"
tab2.tab_level=1
tab2.tab_tip=u"系統登出"
tab2.tab_order=99
tab2.put()

在這個 tabs.py 程式中, 資料類別 Tabs 繼承了 db.Model 類別, 並定義了 6 個屬性 (property), 然後先呼叫 Tabs() 建構子建立資料物件 tab, 對屬性賦值後呼叫 put() 方法將 tab 物件儲存至 Datastore 中成為一個資料實體 (Entity), 此處我們儲存了 2 個資料實體. 注意, 這裡只要有中文的部分前面都加了一個 u, 這樣做是為了將中文從原先的 byte string  轉成 unicode string 之故, 因為這兩種字串不能混用, 而程式一開始就宣告 # -*- coding: utf-8 -*-, 如此一來所有的中文字串都會以 utf-8 編碼為 byte string, 而其他字串則為 unicode string, 因此執行時會出現如下錯誤 :

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7 in position 0: ordinal not in range(128)

只要如上在每一個中文屬性值前面加上 u 改為 unicode string 就可以了, 詳細參閱 :



  <div id='tabs'>
    <ul>
      <li title='系統管理'>
        <a href='/tab_admin'>管理</a>
      </li>
      <li title='系統登出'>
        <a href='/tab_logout'>登出</a>
      </li>
    </ul>
  </div>
  <script language="JavaScript">
    $(document).ready(function(){
      $(this).attr("title","jQuery Tabs Test");
      $("#tabs").tabs();
      });
  </script>


事實上, Datastore 定義了 24 種 Property 類別, 可用來指定 Entity 各屬性的資料型態, 請參考 :

# Datastore 的 24 種 Property 類別

首先要在 "如何在 GAE 中處理 HTTP 要求" 的範例 3/4 基礎上修改 Members 類別, 擴充其屬性以便對這 24 種屬性進行測試, 如下列範例 1 所示 :

註 : 以上寫於 8/8/2014, 母親住院時的 31 病房.


20160129 補充 :

資料物件實體存入 Datastore 時必須注意兩件事 :

  1. 最好要指定 key_name 屬性 :
    通常是指定不會重複, 可用來作為 primary key 的欄位值做為 key_name 值, 這樣在更新資料或刪除資料時要抓出這筆資料就很方便. 
  2. 使用建立物件同時賦值方式 :
    這種方式搭配指定 key_name 可以確保不會有許多重複的 entity 存在於資料儲存庫中. 不要用上面先建立空的資料物件, 再一一指配屬性值的方式, 此法即使有指定 key_name 似乎無法避免產生許多重複的資料物件.

正確做法如下 :

tab1=Tabs(key_name="admin",tab_name="admin",
   tab_label=u"管理", tab_link="/tabtest_admin", tab_level=9,
   tab_tip=u"系統管理", tab_order=99)
tab1.put()


參考 :

# Google App Engine HTTP請求處理 
# Google App Engine 及 Web Programming 簡介 
# Python 沒有 switch - case  
# 用 Python來開 Microsoft Access的.mdb資料檔的中文欄位問題




沒有留言 :