tag:blogger.com,1999:blog-41776435081045682522024-03-14T02:54:01.167+08:00Google App Engine 最佳實踐ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.comBlogger12125tag:blogger.com,1999:blog-4177643508104568252.post-57117043275163026292010-08-27T15:38:00.000+08:002010-08-27T15:38:28.927+08:00App Engine 上的時區問題在 App Engine 上如果要儲存時間,在 model 中就會用 DateTimeProperty, DateProperty 或 TimeProperty 來做為資料型態,比如說:<br />
<blockquote><code><pre class="python" name="code">...
from google.appengine.ext import db
class FooModel(db.Model):
...
...
created = db.DateTimeProperty(auto_now_add=True)
</pre></code></blockquote>以 <code>DateTimeProperty</code> 來說,它對應到的資料結構是 python 標準函式庫中的 <code>datetime.datetime</code>,不過,當你加上了 <code>auto_now_add=True</code> 這個設定之後,每當建立一個新的 entity 時,Datastore API 就會自動加上「現在時間」,也就是 <code>datetime.datetime.now()</code> 的資料,但是 App Engine 所使用的預設時區是 UTC(也就是 GMT+0),簡單地說,如果你的網站使用者都是台灣的用戶,那就會感覺到這個時間晚了八個小時(因為台灣的時區是 CST,也就是 GMT+8),如果要處理這個問題,這裡提供兩個可能的解決方法:<br />
<br />
<h3>不要依賴自動產生的時間,DIY</h3>第一個解決方法,就是在產生/儲存 entity 時不要讓 Datastore API 幫你產生現在的時間,每次在產生或是更新 entity 資料時,自己手動產生出正確的時間。在解決問題之前,首先要製作一個台灣時區的 <code>tzinfo</code>,才可以去改變 <code>datetime.datetime</code> 所表示的時區:<br />
<br />
<blockquote><code><pre class="python" name="code">...
from datetime import tzinfo, timedelta
class TaiwanTimeZone(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=8)
def tzname(self, dt):
return 'CST'
def dst(self, dt):
return timedelta(hours=0)
</pre></code></blockquote><br />
接著,在產生或更新 entity 資料時:<br />
<blockquote><code><pre class="python" name="code">...
foo = FooModel(......,
created=datetime.datetime.now(TaiwanZone()))
foo.put()
</pre></code></blockquote>這樣就會用台灣時區(GMT+8)的時間來儲存資料了。<br />
<br />
<h3>在輸出時用 filter 換掉</h3>另外一個方式,就是在 template 要輸出時再換掉時區,當然還是要準備一個 <code>TaiwanTimeZone</code> 的 <code>tzinfo</code> 類別,然而在儲存資料時,還是使用預設的 UTC 時區去處理,但是可以自訂一個 template filter (詳細的作法請見<a href="http://practicalappengine.blogspot.com/2010/08/filter.html" target="_blank">這篇文章</a>)在 template 中顯示時動態換掉時間及時區:<br />
<blockquote><code><pre class="python" name="code"># get template register
register = webapp.template.create_template_register()
@register.filter
def twtz(value):
from datetime import datetime, timedelta
return (value + timedelta(hours=8)).replace(tzinfo=TaiwanTimeZone())
</pre></code></blockquote>這樣只要在 template 中加上這個 filter 來顯示就可以了。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com2tag:blogger.com,1999:blog-4177643508104568252.post-60428583206616254382010-08-21T16:57:00.000+08:002010-08-21T16:57:42.935+08:00自訂範本系統中的 filter在 App Engine 上製作網頁時,若是沒有使用 django 或是其它的 template 函式庫,應該都會直接使用 App Engine 所提供的 <a href="http://code.google.com/intl/en/appengine/docs/python/gettingstarted/templates.html" target="_blank">django template wrapper</a>(用的是 django 0.96 版的 template),不過這就沒辦法(很簡單地)照著 django 所提供的方式自訂標籤及 filter。不過 App Engine 還是有提供自訂 filter 的方式,只要按照下列的步驟(假設你應用程式的目錄是在 <code>$APP</code>):<br />
<ol><li>建立一個 Python 模組來定義 filters:<br />
<blockquote><code><pre class="python" name="code"># $APP/my_filters.py
from google.appengine.ext import webapp
# 取得 template filters register
register = webapp.template.create_template_register()
@register.filter
def tolower(string_value):
return string_value.lower()
</pre></code></blockquote>這樣一來便建立了一個自訂的 filter: <code>tolower</code>,等等便可以用在 template 中。<br />
</li>
<li>雖然建立好了自訂的 filter(s),但是 App Engine 的 template 函式庫還不知道有這個東西的存在,所以在使用範本引擎輸出前,記得加入下列的程式碼註冊你自訂的 module(s):<br />
<blockquote><code><pre class="python" name="code">...
from google.appengine.ext.webapp import template
# 註冊自訂 filters 的模組(載入模組的名稱)
template.register_template_library('my_filters')
</pre></code></blockquote></li>
<li>完成以上步驟後,就可以在範本中像這樣來使用自訂的 filters:<br />
<blockquote><code><pre class="html" name="code">...
Description: {{ description|tolower }}
</pre></code></blockquote></li>
</ol><br />
如此一來,便不必在 request handler 中預先處理輸出的內容,可以把這部份的程式碼用 filter 來解決。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com0tag:blogger.com,1999:blog-4177643508104568252.post-91083247474803386742010-07-14T22:00:00.000+08:002010-07-14T22:00:53.879+08:00使用 OpenID 作為帳號驗證AppEngine 在 1.3.4 版本之後,開始實驗支援 OpenID 的身份驗證,除了可以在建立 app 時選擇使用 OpenID 作帳號驗證之外,也可以在後台設定。<br />
<br />
<img alt="" src="http://code.google.com/appengine/docs/images/authoptions.png" /><br />
<small>建立 app 時可以選擇使用 Open ID</small><br />
<br />
<img border="0" src="http://3.bp.blogspot.com/_hNy_9UI1_R8/TD2-ZueVAGI/AAAAAAAARD0/M32SgwmMWRA/s320/auth.png" /><br />
<small>在管理後台的 Administration > Application Settings 中設定</small><br />
<br />
設定好了之後,其實程式也不用修改太多,還是可以直接使用 <code>google.appengine.api.users</code> 模組中的函式來做身份認證,App Engine 已經實作了 Open ID 的規格,所以可以根據 Open ID 找出認證網址,但首先要在 app.yaml 檔案中加入 <code>/_ah/login_required</code> 的 URL 像是這樣:<br />
<blockquote><code><pre class="yaml" name="code">(app.yaml)
...
- url: /_ah/login_required
script: openid_login.py
...</pre></code></blockquote><br />
然後在你的登入頁面中,將登入的動作導向 <code>/_ah/login_required</code> 這個 URL,而處理的程式就像這樣:<br />
<br />
<blockquote><code><pre class="python" name="code">(openid_login.py)...
from google.appengine.ext import webapp
from google.appengine.api import users
class OpenIdHandler(webapp.RequestHandler):
def get(self):
....
# 使用者輸入的 Open ID URL
openid_url = self.request.get('openid')
# Open ID 認證結束後導向的 URL
continue_url = '....'
# 將使用者導向 Open ID provider 的認證網址:
self.redirect(users.create_login_url(continue_url, None, openid_url))
...</pre></code></blockquote>如果 <code>openid_url</code> 是空值,則 App Engine 會利用 Google Account API 來完成認證。<br />
<br />
當使用者用 OpenID 認證成功之後,就可以使用利用下面的方式來取得使用者的 OpenID 資訊:<br />
<br />
<blockquote><code><pre class="python" name="code">...
from google.appengine.api import users
...
user = users.get_current_user()
if user:
# 取得 openid identity
id = user.federated_identity()
# 取得 openid provider URL
provider = user.federated_provider()
...</pre></code></blockquote>ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com0tag:blogger.com,1999:blog-4177643508104568252.post-78050122317630507182010-01-07T22:04:00.002+08:002010-01-07T22:17:55.150+08:00[Mac] 在 Snow Leopard 上開發 Google App EngineMac OSX 在 Snow Leopard (10.6) 之後,已經將預設的 python 設定為 2.6,不過系統還是有安裝 2.5 版本,所以開發基本上沒有什麼問題,只是要稍微作一些調整:<br /><ol><br /><li>如果你的電腦還沒有安裝過 <a href="http://developer.apple.com/tools/xcode/" target="_blank">XCode</a>(Mac 系統安裝光碟內及iPhone SDK 都有),必須要先安裝,讓系統有安裝編譯的工具</li><br /><li>雖然系統內建了 Python 2.5,不過並沒有安裝 <a href="http://www.pythonware.com/products/pil/" target="_blank">PIL</a> 這個 Python 處理影像的函式庫,因為 App Engine 中的 image API 會用到 PIL,所以也要安裝這個函式庫。為了讓函式庫能支援 JPEG 檔案的處理,所以就要先來安裝 libjpeg。</li><br /><li>首先到<a href="http://www.ijg.org/" target="_blank">這裡</a>下載 <code>jpegsrc.v7.tar.gz</code> 檔案,然後在文字模式下依照下列步驟編譯及安裝:<blockquote><code><br />$ tar zxvf jpegsrc.v7.tar.gz<br />....<br />$ cd jpeg-7<br />$ export CC=/usr/bin/gcc-4.0<br />$ ./configure --enable-shared --enable-static<br />$ make<br />$ sudo make install</code></blockquote></li><br /><li>如果一切都很順利的話,那就可以到 PIL 網站下載 <code>Python Imaging Library 1.1.6 Source Kit</code> 原始檔案回來編譯:<br /><blockquote><code><br />$ tar zxvf Imaging-1.1.6.tar.gz<br />...<br />$ cd Imaging-1.1.6<br /># 將 setup.py 檔案中找到 JPEG_ROOT 然後改成 JPEG_ROOT = "/usr/local/lib"<br />$ /usr/bin/python2.5 setup.py build<br />$ sudo /usr/bin/python2.5 setup.py install</code></blockquote></li></ol><br />如果一切都沒有問題的話,那應該就沒什麼問題了。只是記住當你在啟動 <code>dev_appserver.py</code> 時,要使用 <code>/usr/bin/python2.5</code> 來啟動,而不要使用 <code>/usr/bin/python</code> 以免用到 Python 2.6 版。<br /><br />若是使用了 <b>GoogleAppEngineLauncher.app</b> 這個應用程式的話,可以在 Preferences... 中設定 Python 的路徑為 <code>/usr/bin/python2.5</code>ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com6tag:blogger.com,1999:blog-4177643508104568252.post-57459801964769845782009-08-10T13:24:00.002+08:002009-08-10T14:04:55.081+08:00減輕資料查詢、更新的負擔在 Google App Engine 上開發應用程式,很多人會面臨到使用額度(Quota)的問題,所以開發者在將應用程式上線後,必須不斷觀察應用程式的存取狀況,以便隨時調整應用程式,避免某一項使用額度過份衝高。<br /><br />這篇文章要介紹的是--儘可能地減少資料查詢、更新的動作,因為 Datastore API 的呼叫次數有限制,而且一個 request 的處理時間也有 30 秒的限制,如果在操作資料時沒有注意到一些細節,可能就會碰到問題。<br /><h4>平行讀取、更新或刪除 data entities</h4><br />在「<a href="http://practicalappengine.blogspot.com/2009/07/blog-post.html" target="_blank">避免資料寫入衝突</a>」這篇文章中,提到可以使用 App Engine 所提供的 <code>memcache</code> API 來作資料快取,這樣也是一種減少呼叫 Datastore API 的策略。<br /><br />除此之外,善加利用每個 data entity 的 key 也可以達到減少呼叫 Datastore API 的目標。例如以下這段程式碼:<br /><blockquote><code><pre class="python" name="code">...<br /># 修改許多 data entity<br />for product in products:<br /> product.price = product.price * 1.1<br /> product.put()<br />...</pre></code></blockquote><br />在這個例子中,<code>products</code> 中有多少個 entities,就會呼叫多少次 <code>put()</code>,但若是將上述的程式碼修改為:<br /><blockquote><code><pre class="python" name="code">...<br />from google.appengine.ext import db<br /># 修改許多 data entity<br />for product in products:<br /> product.price = product.price * 1.1<br />db.put(products)</pre></code></blockquote><br />由於 <code>db.put()</code> 函式支援 list 資料型態的參數,所以這樣的作法僅會算作一次 Datastore API 的呼叫,比起上述的方式大大節省了 Datastore API 的呼叫次數。<br /><br />讀取資料時也是以此類推,若原本的程式為:<br /><blockquote><code><pre class="python" name="code">...<br />products = []<br /># keys 為一個 key list<br />for key in keys:<br /> products.append(db.get(key))<br /></pre></code></blockquote><br />也可以改寫成:<br /><blockquote><code><pre class="python" name="code">...<br />from google.appengine.ext import db<br /># keys 為一個 key list<br />products = db.get(keys)<br /></pre></code></blockquote><br />同理可證,若要同時刪除許多 entities,也可以使用 <code>db.delete</code>。<br /><h4>有效率地使用 GQL</h4><br />另外,若是要使用 <code>GqlQuery</code> 作資料查詢時,若是只需要取出 entities 的 key 值,在 GQL 查詢語句中僅需取出 <code>__key__</code> 欄位即可:<br /><blockquote><code><pre class="python" name="code">...<br />from google.appengine.ext import db<br /><br />product_keys = db.GqlQuery('SELECT __key__ FROM Product WHERE title = :title', title='...')<br /># 或是 product_keys = Product.gql('WHERE title = :title', title='...', keys_only=True)<br /></pre></code></blockquote><br />如此一來,讀取的時間就會比讀取全部的欄位還要快許多(如果資料量夠多的話...)<br /><br />另外,若是查詢語句會重複使用,可以將該語句建立成一個 <code>GqlQuery</code> 物件後,再利用 <code>bind()</code> 方法重新利用該查詢語句。例如本來的程式碼可能是:<br /><blockquote><code><pre class="python" name="code">...<br />from google.appengine.ext import db<br /><br />conditions = [['x', 'y'], ['1', '2'], .....]<br />for cond in conditions:<br /> query = db.GqlQuery('SELECT * FROM Foo WHERE first = :first, second = :second', first=cond[0], second=cond[1])<br /> ....<br /></pre></code></blockquote><br />如此一來,迴圈每執行一次就會建立一個 <code>GqlQuery</code> 物件,資料查詢會變得很沒有效率,如果碰到這樣的情況,程式碼應該改寫為:<br /><blockquote><code><pre class="python" name="code">...<br />from google.appengine.ext import db<br /><br />conditions = [['x', 'y'], ['1', '2'], .....]<br />prepared_query = db.GqlQuery('SELECT * FROM Foo WHERE first = :first, second = :second')<br />for cond in conditions:<br /> query = prepared_query.bind(first=cond[0], second=cond[1])<br /> ....<br /></pre></code></blockquote><br />如此便能重複利用 <code>GqlQuery</code> 這個物件了。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com2tag:blogger.com,1999:blog-4177643508104568252.post-82455502546490250062009-08-05T14:33:00.017+08:002009-08-05T15:29:05.268+08:00使用 Eclipse + PyDev 開發 Google App Engine 專案在 Windows 上開發應用程式,大多數的開發者會藉著使用 <abbr title="Integrated Development Environment">IDE</abbr> 來輔助開發,簡化一些設定、啟動或測試等步驟。而在 Windows 上若要開發 Google App Engine 的應用程式,使用 <a href="http://eclipse.org/" target="_blank">Eclipse</a> 作為 IDE,並且搭配 <a href="http://pydev.sourceforge.net/" target="_blank">PyDev</a> 這個 Eclipse 的 plugin,會簡化許多開發的設定,尤其是最新的 PyDev 甚至還直接支援了 Google App Engine 專案呢!以下就為各位做個簡單的介紹。<br /><br /><h4>安裝及設定</h4><br />在開始之前,先確定您的 Windows 環境已經安裝了 <a href="http://java.sun.com/javase/downloads/index.jsp" target="_blank">Java SDK (JDK)</a> 以及 <a href="http://python.org/download/releases/2.5.4/">Python</a> (目前 Google App Engine 僅支援 Python 2.5.x) 。Java 是為了執行 Eclipse,而 Python 當然就是為了用來執行 Google App Engine 的專案囉。<br /><br />首先,到 <a href="http://eclipse.org/downloads/">Eclipse 的官方網站</a>下載 Eclipse Classic 3.5.0<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snkpz8rLG6I/AAAAAAAACok/dbPYRbYuSWQ/s1600-h/download_eclipse.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 320px; height: 227px;" src="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snkpz8rLG6I/AAAAAAAACok/dbPYRbYuSWQ/s320/download_eclipse.png" alt="" id="BLOGGER_PHOTO_ID_5366366403425934242" border="0" /></a><br /><br />下載回來後,將 zip 檔案解壓縮,執行 <b>eclipse</b> 目錄中的 <code>eclipse.exe</code> 便可以開啟 Eclipse:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnksaQQ6XhI/AAAAAAAACos/UyZS26T3KIs/s1600-h/eclipse_ide.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 320px; height: 240px;" src="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnksaQQ6XhI/AAAAAAAACos/UyZS26T3KIs/s320/eclipse_ide.png" alt="" id="BLOGGER_PHOTO_ID_5366369260542778898" border="0" /></a><br /><br />開啟 Eclipse 之後,首先將環境的設定作點修改,從功能表列的 <b>Window</b> » <b>Preferences</b> 進入設定畫面,首先設定用空白取代 tab 字元,因為 Python 對於程式碼的縮排有嚴格的一致性,所以為了避免不必要的煩惱,在 <b>General</b> » <b>Editor</b> » <b>Text Editors</b> 的設定中,<em>將 tab 取代為 4 個空白字元</em>。<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_hNy_9UI1_R8/Snktff_veXI/AAAAAAAACo0/STgI29iOZJc/s1600-h/s2t.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 320px; height: 155px;" src="http://4.bp.blogspot.com/_hNy_9UI1_R8/Snktff_veXI/AAAAAAAACo0/STgI29iOZJc/s320/s2t.png" alt="" id="BLOGGER_PHOTO_ID_5366370450176702834" border="0" /></a><br /><br />除此之外,也將由 Eclipse 所建立的專案,調整成<em>預設使用 UTF-8 作為字元編碼</em>,以及<em>使用 UNIX 換行字元</em>:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_hNy_9UI1_R8/SnkuHtoA21I/AAAAAAAACo8/kkmgEbGCLiw/s1600-h/charset_and_line.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 74px;" src="http://2.bp.blogspot.com/_hNy_9UI1_R8/SnkuHtoA21I/AAAAAAAACo8/kkmgEbGCLiw/s320/charset_and_line.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366371141030042450" /></a><br /><br /><br />為了讓 Eclipse 能夠作為開發 Python 專案的 IDE,此時還需要安裝 PyDev 這個 plugin,可以從<a href="http://pydev.sourceforge.net/download.html" target="_blank">PyDev 的官方網站</a>上看到安裝 URL(如:http://nightly.aptana.com/pydev/site.xml),將這個 URL 複製下來,回到 Eclipse,到功能表列的 <b>Help</b> » <b>Install New Software...</b>,將剛才複製的 URL 貼在 <em>Work with:</em> 的文字框中,並按下 <b>Add</b> 按鈕:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnkvgToWffI/AAAAAAAACpM/8aNppn16Gsc/s1600-h/Add+PyDev+Repo.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 297px;" src="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnkvgToWffI/AAAAAAAACpM/8aNppn16Gsc/s320/Add+PyDev+Repo.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366372663060495858" /></a><br /><br />接著就是勾選 PyDev 然後將它安裝完成,安裝完畢後,Eclipse 便會請你重新啟動或是套用變更將 plugin 完成整合到 Eclipse 中。<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_hNy_9UI1_R8/Snkv5slfw6I/AAAAAAAACpU/vibYwC4y84Y/s1600-h/Install+PyDev.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 297px;" src="http://1.bp.blogspot.com/_hNy_9UI1_R8/Snkv5slfw6I/AAAAAAAACpU/vibYwC4y84Y/s320/Install+PyDev.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366373099256136610" /></a><br /><br />安裝完 PyDev 之後,別忘了先設定 PyDev,讓它瞭解 Python 被安裝在哪裡:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_hNy_9UI1_R8/SnkyMKN5e_I/AAAAAAAACpk/-1sj8cv6-YQ/s1600-h/setup+pydev.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 275px;" src="http://1.bp.blogspot.com/_hNy_9UI1_R8/SnkyMKN5e_I/AAAAAAAACpk/-1sj8cv6-YQ/s320/setup+pydev.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366375615471123442" /></a><br /><br /><h4>建立 Google App Engine 專案</h4><br />設定完 Python 之後,建立新專案時,就有 <b>PyDev Google App Engine Project</b> 可以選擇了:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snkxh25_SjI/AAAAAAAACpc/7hLyTR9nC4I/s1600-h/pydev_gae.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 318px;" src="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snkxh25_SjI/AAAAAAAACpc/7hLyTR9nC4I/s320/pydev_gae.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366374888732838450" /></a><br /><br />輸入專案名稱,以及別忘了選擇正確的 Python 版本:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnkynQkaE_I/AAAAAAAACps/Hrg-ZRiBGww/s1600-h/create+gae+proj.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 302px; height: 320px;" src="http://3.bp.blogspot.com/_hNy_9UI1_R8/SnkynQkaE_I/AAAAAAAACps/Hrg-ZRiBGww/s320/create+gae+proj.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366376081032614898" /></a><br /><br />接著選擇 Google App Engine 的安裝位置:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snky-_DPqHI/AAAAAAAACp0/tll4YqnG6tU/s1600-h/setup+gae.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 302px; height: 320px;" src="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snky-_DPqHI/AAAAAAAACp0/tll4YqnG6tU/s320/setup+gae.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366376488646977650" /></a><br /><br />最後就是填入你的 application ID 及專案的範本:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_hNy_9UI1_R8/SnkzcTOYrvI/AAAAAAAACp8/RW-thzrti-Y/s1600-h/app+id+and+template.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 302px; height: 320px;" src="http://1.bp.blogspot.com/_hNy_9UI1_R8/SnkzcTOYrvI/AAAAAAAACp8/RW-thzrti-Y/s320/app+id+and+template.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366376992278621938" /></a><br /><br />如此一來專案就建立完成了。<br /><br /><h4>執行及部署</h4><br />當你的程式寫完,想要啟動開發用伺服器來作測試時,在專案視窗中的 <b>src</b> 目錄上按下右鍵,選擇 <b>Run as...</b> 就有 <b>Run: Google App</b> 可以選擇了:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snk0GnigXnI/AAAAAAAACqE/X6mIIVJMVrM/s1600-h/run+gae.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 254px;" src="http://2.bp.blogspot.com/_hNy_9UI1_R8/Snk0GnigXnI/AAAAAAAACqE/X6mIIVJMVrM/s320/run+gae.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366377719286226546" /></a><br /><br />如果要將應用程式部署至 Google App Engine 上,則一樣在 <b>src</b> 目錄上按下右鍵,選擇 <b>PyDev: Google App Engine</b> 就有 <b>Upload</b> 及 <b>Manage</b> 可以使用。<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_hNy_9UI1_R8/Snk0nnaLpGI/AAAAAAAACqM/QDATDk52zug/s1600-h/deploy+gae.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 278px; height: 320px;" src="http://1.bp.blogspot.com/_hNy_9UI1_R8/Snk0nnaLpGI/AAAAAAAACqM/QDATDk52zug/s320/deploy+gae.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5366378286186996834" /></a><br /><br />這樣是不是簡單多了呢?祝各位開發愉快 :-)ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com6tag:blogger.com,1999:blog-4177643508104568252.post-42923640816066424442009-07-22T23:57:00.000+08:002009-07-22T23:57:24.447+08:00避免資料寫入衝突在使用 App Engine 時,常常會建立 model 來作 sequence(如:手動配置資料的 id)、counter(計數器)之類的工作,以前面文章提到的 counter 為例,如果 <code>Counter</code> 在 datastore 中的 data entity 只有一個,每次新增資料時,程式都會到同一個地方作資料寫入(update 也是一種 write)的動作,如果新增資料的頻率很高,那就很容易發生寫入衝突(write contention)而使得程式的效能低落。<br /><br />試想你在作一個類似 <a href="http://twitter.com/" target="_blank">Twitter</a> 的網站,使用者增加訊息的速度是非常驚人的,如果只使用單一個 data entity 來儲存、更新計數器,那每則訊息的更新時間就會變長,因為都在等待更新計數器的動作。如果你曾經學習過用在磁碟機上的 <a href="http://en.wikipedia.org/wiki/RAID" target="_blank"><acronym title="Redundant Array of Inexpensive Disks">RAID</acronym></a> 技術,那便可以借鏡 RAID 5 規格中分散資料擺放的方式,使得各個讀寫資料的動作可以同時進行。<br /><br />以計數器為例,可以試著把計數器拆開成好幾個碎片(shard),在更新計數器時,可以只要隨便選一個碎片來更新就可以了,如果要取得完整的數值,只要把各碎片的值加總就得到答案了。<br /><br />這樣的概念,程式要怎麼寫呢?首先把 <code>Counter</code> 的定義修改成這樣:<br /><blockquote><code><pre class="python" name="code">from google.appengine.ext import db<br /><br />class Counter(db.Model):<br /> name = db.StringProperty(required=True)<br /> count = db.IntegerProperty(required=True, default=0)</pre></code></blockquote><br />增加了一個 <code>name</code> 欄位,這是為了標記碎片是屬於哪一個計數器,也就是 <code>name</code> 欄位相同的碎片就是屬於同一個計數器。有了這樣的 model 之後,更新計數器的程式碼可以改寫成這樣:<br /><blockquote><code><pre class="python" name="code">...<br />import random<br /><br />shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片<br />counter = Counter.get_by_key_name(shard_name)<br />if counter is None:<br /> counter = Counter(key_name=shard_name, name='product_counter')<br />counter.count += 1<br />counter.put()</pre></code></blockquote><br />上面的例子是我預計將計數器切成 4 個碎片,然後每個「碎片 entity」的 <code>key_name</code> 分別為 <code>product_counter_0</code>, ..., <code>product_counter_3</code>,於是便能夠直接根據 <code>key_name</code> 將碎片的 entity 取出,若是碎片不存在就立刻產生一個新的 entity,而新的 entity 除了要設定 <code>key_name</code> 之外,最重要的就是要記得標上 <code>name</code> 欄位的值,因為當我們需要加總計數器時,就是靠著這個欄位來取出各個碎片。最後則是將 <code>count</code> 欄位的值加 1 後儲存,完成更新計數器的動作。<br /><br />接下來要處理的就是加總的動作囉,因為可以藉著 <code>name</code> 欄位來查詢,所以程式也變得很簡單:<br /><blockquote><code><pre class="python" name="code">...<br />counters = Counter.all().filter('name =', 'product_counter')<br />total = 0<br />for counter in counters:<br /> total += counter.count</pre></code></blockquote><br />所以 <code>total</code> 的值就是計數器正確的數值了。<br /><br /><h4>改善效能</h4><br />上面的例子雖然改善了寫入衝突的問題,但光是這樣作,除了每次讀取計數器數值時都要做一次查詢(耗費 Datastore API 呼叫次數)以外,碎片如果太多也會增加計算時間,為了改善這個問題,可以為計數器作 cache。<br /><br />Google App Engine 有提供 Memcache 的服務,我們可以直接拿它來作為計數器的 cache,首先從計數器的加總下手,一旦數值被計算出來後就放進 cache,如此一來,讀取計數器數值時就可以先從 cache 讀取,不在 cache 裡才去作資料查詢:<br /><blockquote><code><pre class="python" name="code"># 讀取計數器總數時,使用 cache<br />from google.appengine.api import memcache<br /><br />counter_name = 'product_counter'<br />total = memcache.get(counter_name) # 這裡用 counter_name 作為 cache key<br />if total is None:<br /> total = 0<br /> counters = Counter.all().filter('name =', counter_name)<br /> for counter in counters:<br /> total += counter.count<br /> memcache.set(counter_name, total)</pre></code></blockquote><br /><br />那當計數器被更新的時候怎麼辦?除了更新 entity 之外,若是計數器的 cache 還存在,可以直接把 cache 裡的值加 1,這樣計數器的總數就不必重新計算了:<br /><blockquote><code><pre class="python" name="code"># 更新計數器時,別忘了更新 cache<br />from google.appengine.api import memcache<br />import random<br /><br />counter_name = 'product_counter'<br />shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片<br />counter = Counter.get_by_key_name(shard_name)<br />if counter is None:<br /> counter = Counter(key_name=shard_name, name=counter_name)<br />counter.count += 1<br />counter.put()<br /><br /># 更新 cache<br />total = memcache.get(counter_name)<br />if total:<br /> memcache.incr(counter_name) # 使用 incr 函式直接對數值資料加 1 </pre></code></blockquote><br />這樣就不必擔心更新了計數器,卻忘記更新快取了。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com2tag:blogger.com,1999:blog-4177643508104568252.post-62322897918575960042009-07-06T13:30:00.000+08:002009-07-06T14:03:30.424+08:00支援交易運算的計數器<h4>支援交易運算</h4><br />在許多資料庫運算中,為了保持相關的資料庫運算結果能夠不被分割地將同時送進資料庫(也就是一旦有一個操作發生失敗,則整串操作都會同時取消),所以提供了「交易」(transaction)的運算。<br /><br />Google App Engine 上的 datastore 當然也支援「交易」運算,在上述的例子中,共有兩個資料庫運算--<code>Product</code> entity 的寫入以及 <code>Counter</code> entity 的更新,試著想像這兩個操作若各自(或同時)發生錯誤時會有什麼問題,若是 <code>Product</code> entity 在寫入時發生錯誤,程式就中斷了,那就沒什麼太大的問題,若是 <code>Product</code> entity 成功寫入 datastore,但 <code>Counter</code> entity 在更新時發生錯誤,這時就會造成計數器的統計數量不一致,這時候就適合使用交易運算了。<br /><br /><h4>交易運算的限制</h4><br />在 Google App Engine 中,交易運算只能操作相同 entity group 的資料,如果我們要將 <code>Product</code> 及 <code>Counter</code> 的新增動作在同一個交易運算中完成的話,就必須讓它們成為同一個 entity group。<br /><br />要成為同一個 entity group,不管是 <code>Product</code> 還是 <code>Counter</code> 都需要一個共同的 parent,因此需要一個額外的 data model 來作為這個 entity group 的 root,這裡我提供一個 <code>Index</code> 的 model,順便用來作為每一筆 <code>Product</code> 資料的「序號產生器」 :p<br /><blockquote><code><pre class="python" name="code">class Index(db.Model):<br /> max_index = db.IntegerProperty(required=True, default=0)</pre></code></blockquote><br />有了這個 model 後,新增 <code>Product</code> 及更新 <code>Counter</code> 的動作就可以改寫為:<br /><blockquote><code><pre class="python" name="code"># 新增 product 的程式片段...<br />...<br /># 從 Index 中取出 Product 的 index<br />ind = Index.get_by_key_name('product')<br />if ind is None:<br /> ind = Index()<br />ind.max_index += 1<br />ind.put()<br /><br /># 新增 product entity 並設定 parent 為 ind<br />p = Product(parent=ind, key_name="product_%d" % ind.max_index,........) <br />p.put()<br /><br /># 根據 key_name 取得 counter,並且指定 parent<br />counter = Counter.get_by_key_name('product_counter', parent=ind.key())<br />if counter is None:<br /> # 如果 counter 不存在,則建立一個新的,別忘了指定 key_name 及 parent<br /> counter = Counter(parent=ind, key_name='product_counter')<br /><br />counter.count += 1<br />counter.put()</pre></code></blockquote><br />在新增 <code>Product</code> 及 <code>Counter</code> 時,都加上指定 <code>parent</code> 的參數,以此將這些資料建構成一個 entity group,然而,一旦資料在 entity group 中,在取出時也要加上 <code>parent</code> 參數。<br /><br /><h4>作成交易運算的函式</h4><br />因此,我們可以把上面的程式碼包成一個函式,比方說是 <code>create_product</code>,這樣就可以利用 Datastore API 中的 <code>run_in_transaction</code> 函式來作成交易運算了。<br /><br /><blockquote><code><pre class="python" name="code">....<br />def create_product():<br /> # 放入上述的程式碼<br /><br />....<br /># 新增資料時...<br />from google.appengine.ext import db<br />db.run_in_transaction(create_product)<br /></pre></code></blockquote><br /><br />如此一來,只有當 <code>Index</code>, <code>Product</code> 及 <code>Counter</code> 的資料操作都成功時,更新的資料才會進入 datastore 中。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com0tag:blogger.com,1999:blog-4177643508104568252.post-87529558926744408342009-06-18T23:41:00.000+08:002009-06-18T23:41:24.950+08:00資料計數器<h3>Datastore API 的限制</h3><br />在使用 Datastore API 時有個限制,就是每作一次 query ,最多能存取到的資料數量為 1000 筆(註1),也因為這個限制,所以也不能直接使用 <code>Query</code> 物件下的 <code>count()</code> 方法來計算資料數量。<br /><br />舉例來說,如果要設計一個網路商店,每一件商品是一個 <code>Product</code> entity,假如網站中的商品數量超過 1000 筆,那這樣的程式碼:<br /><blockquote><code><pre class="python" name="code">from google.appengine.ext import db<br /><br />class Product(db.Model):<br /> ...<br /> # product 各欄位<br /><br />...<br />query = Product.all()<br />number_of_products = query.count()</pre></code></blockquote><br />並不會幫你統計出究竟有多少 <code>Product</code> entity,因為 <code>query</code> 的結果最多就是 1000 筆資料。<br /><br /><h3>手動記錄</h3><br />為了解決這樣的問題,其實可以自己定義一個「計數器」的資料,用來統計究竟有多少資料,於是立刻可以寫出這樣的 data model:<br /><blockquote><code><pre class="python" name="code">from google.appengine.ext import db<br /><br />class Counter(db.Model):<br /> count = db.IntegerProperty(required=True, default=0)</pre></code></blockquote><br />有了這樣的資料模型,便能夠用來統計網站中任何資料的數量。我們可以經由不同的 <code>key_name</code> 來區分出不同的計數器,根據上述的例子,便可以在新增 <code>Product</code> 時,在計數器上加1以便統計。<br /><blockquote><code><pre class="python" name="code"># 新增 product 的程式片段...<br />...<br />p = Product(........) # 新增 product entity<br />p.put()<br /><br /># 根據 key_name 取得 counter<br />counter = Counter.get_by_key_name('product_counter')<br />if counter is None:<br /> # 如果 counter 不存在,則建立一個新的,別忘了指定 key_name<br /> counter = Counter(key_name='product_counter')<br /><br />counter.count += 1<br />counter.put()<br /></pre></code></blockquote><br />以此類推,當要刪除一個或多個 <code>Product</code> entities 時,也要同步更新對應的計數器資料,以保持資料數量的一致性(consistency)。<br /><br /><hr><br /><h6>註</h6><ol><li>之後會介紹如何讀出超過1000筆的資料</li></ol>ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com2tag:blogger.com,1999:blog-4177643508104568252.post-85100204256553300892009-06-18T17:10:00.003+08:002009-06-18T17:17:33.251+08:00屬性中的 indexed=False如果你的程式中,有個 data model 的 property 很多,那資料在存取時會隨著 property 數量的成長而降低效能(尤其是發生在呼叫 <code>put()</code> 來儲存資料時),這是因為在儲存或更新資料時,datastore 也會針對這些 property 作一些 index 的動作,使得整個操作的時間變長。<br /><br />如果確定不會用某個 property 來作資料查詢 filter 的條件,那麼可以在定義 property 時加上 <code>indexed=False</code> 參數,提示 datastore 這個 property 不要做 index,範例程式如下:<br /><blockquote><code><pre class="python" name="code">from google.appengine.ext import db<br /><br />class Foo(db.Model):<br /> ....<br /> bar = db.StringProperty(indexed=False)</pre></code></blockquote>ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com0tag:blogger.com,1999:blog-4177643508104568252.post-87071046351728018382009-06-15T11:40:00.008+08:002009-06-15T20:22:00.101+08:00為儲存的內容加上標籤(簡單版)現在很多網站都會為儲存的內容加上標籤,像 <a href="http://flickr.com/" target="_blank">Flickr</a> 的每一張相片都可以設定標籤:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_hNy_9UI1_R8/SjXDm12OBnI/AAAAAAAACZ4/eBRmflaNYig/s1600-h/flickr_tags.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 144px; height: 70px;" src="http://1.bp.blogspot.com/_hNy_9UI1_R8/SjXDm12OBnI/AAAAAAAACZ4/eBRmflaNYig/s400/flickr_tags.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5347395204629268082" /></a><br /><br />或是像 delicious 的書籤也可以設定標籤:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_hNy_9UI1_R8/SjXEaa8al5I/AAAAAAAACaA/c63Aim9OZ5A/s1600-h/delicious_tags.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 43px;" src="http://3.bp.blogspot.com/_hNy_9UI1_R8/SjXEaa8al5I/AAAAAAAACaA/c63Aim9OZ5A/s400/delicious_tags.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5347396090760697746" /></a><br /><br />在 App Engine 上開發應用程式,如果要為儲存的內容加上標籤的支援,那麼在資料模型(Data Model)的定義時,可以加上一個 property:<br /><blockquote><code><pre class="python" name="code">from google.appengine.ext import db<br />class MyContent(db.Model):<br /> ....<br /> tags = db.StringListProperty()</pre></code></blockquote><br />如此一來,<code>tags</code>就可以儲存<b>由字串所組成的 list</b>資料,也就是可以儲存 MyContent 物件的標籤。<br /><br />假設使用者透過表單送出標籤的資料,而標籤的資料是一個<em>以逗號(,)隔開的字串</em>,那麼要儲存標籤的作法就是:<br /><blockquote><code><pre class="python" name="code">....<br />tags_string = self.request.get('tags')<br />content = MyContent()<br />....<br />content.tags = map(lambda x: x.strip(), tags_string.split(','))<br />content.put()<br />....</pre></code></blockquote><br />雖然 <code>tags_string.split(',')</code> 已經產生一個由字串所組成的 list 了,但是使用者輸入的字串,可能會在逗號的前後留下空白,所以用了 <code>map</code> 函數將 list 中的每個元素空白消掉(<code>strip</code> 函數)<br /><br />如果要取出含有某個標籤值(如:<code>food</code>)的內容,則可以直接使用 GQL 中特殊的語法來取出:<br /><blockquote><code><pre class="python" name="code">....<br />query = MyContent.gql('WHERE tags = :1', 'food')<br />for content in query:<br /> ....</pre></code></blockquote><br />因為 GQL 的設計,雖然 <code>tags</code> 欄位是一個 list,但是只要比對的元素有出現在 list 中,則 <code>=</code> 運算的結果就會是 <code>True</code>,所以就能夠輕易地根據 list 中的元素來查詢資料。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com2tag:blogger.com,1999:blog-4177643508104568252.post-45668636100771750112009-06-15T11:33:00.002+08:002009-06-15T11:36:23.291+08:00本部落格的宗旨筆者因為深感 Google App Engine 的中文討論資源不足,所以打算把自己的研究整理成專門的 blog 來發表,順便與同好們互相討論學習。<br /><br />未來這個部落格將會不定期更新一些在 Google App Engine 上撰寫程式的技巧及範例,主要會以 Python 版本為主,若有筆誤或是觀念錯誤的部份,還請各位讀者不吝賜教。ericskhttp://www.blogger.com/profile/18370675023130925184noreply@blogger.com0