ここ数ヶ月、Google App Engine/Pythonを使い、初めてちょっとしたものを作ってみているのだけど、開発初期から知っておけばよかったなー、と思うノウハウ/tips的なものをずらずらと書いてみる。
基本的な環境設定は、 以前書いた まま。
0. 公式ドキュメントを良く読む
言うまでもなく、だけど、 マニュアル はもちろん、 この辺 の下の読み物も、流し読みだけでもしておいたほうがいい。
datastoreとmodel的なところ
1. key nameを使いこなす
key nameは、レコードの作成時に指定できる(RDBでいう)primary keyの別名みたいなもの。primary key自体は自動的で作成されるので開発者が指定できるのはkey nameだけ。
key nameをうまく使うことで、datastoreを使いやすくすることができる。特にdatastore上で"unique"を表現したいときに活躍する。
たとえば、 Modelクラスのget_or_insert では、key nameを使うことで簡単にuniqueなレコードを作成することができる。get_or_insert()と言う名前通り、 key nameで指定したレコードが存在したらget、しなかったらそのままinsertといういうことができる。この処理はatomicに行われるので、同じkey nameのレコードが複数存在してしまうこともない。
get_or_insert()の注意点としては、 get時には渡した値ではupdateされない、ということにということがある。そのまんまget or inssert。
2. どういうkey nameを使うべきか
- 他システムから取り込んだレコードでuniqueとされている値
例えば、twitter botとか作るなら(作ってないけど)、twitterのユーザIDとか、tweet毎のIDとかそのまま key nameに入れてしまうのが良さそう。
今作っているシステムでは、別システムのMongoDB上で作ったデータをそのまま取り込んでいる部分がある。そういうデータは MongoDBのBSON ID を文字列にしたのをそのまま key nameにしている。なかなかいい感じ。
- クライアント側で生成されたデータ
クライアント側で生成されるようなデータをdatastoreに格納する場合、クライアント側でUUIDみたいのを作成し、それをそのままkey nameにするのもなかなかよさそう。
このUUIDをkey nameにしてget_or_insert()することで、クライアント端末やネットワークの不調等で同一データが2回POSTされたりしても、データが二重になったりしないのがなかなか気に入っている。もちろんUUIDを偽造されても問題ないような作りにしておく必要はあるけど。
- 複合キー
key nameは一つしか設定できないのだけど、ただの文字列なので上記の値を結合して関連テーブルを作成したりできる。これは場合によってはなかなか便利。
- その他
uniqueっぽいものがないモデルの場合でも、とりあえずランダムで長めの文字列を指定しておくとよいかも。後からは指定できない。
3. GQLで key nameで検索
key nameでのオブジェクトの取得は、基本的には、 get_by_key_name を使えばいいのだけど、WebのadminコンソールとかGQLしか使えないようなところでは、こんな感じに書ける。
WHERE __key__ = KEY('FooModel', 'cff0611529520910195ed357dd69f908')
4. get_or_insert() でgetされたかinsertされたかを知る
get_or_insert() を実行した結果、getされたかinsertされたかがそのままでは簡単にはわからないのが、これを知りたいときがある。例えば「get_or_insert()してgetだった場合には〜する」みたいなとき。
こういうときのために、次のようなclass methodを作っている。
@classmethod
def get_or_insert2(cls, key_name, **kwds):
def txn():
entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
if entity is None:
entity = cls(key_name=key_name, **kwds)
entity.put()
return (True,entity)
return (False,entity)
return run_in_transaction(txn)
または、 insertに成功した場合だけオブジェクトを返し、既に存在する場合はNoneを返す処理
@classmethod
def insert_or_fail(cls, key_name, **kwds):
def txn():
entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
if entity is None:
entity = cls(key_name=key_name, **kwds)
entity.put()
return entity
return None
return run_in_transaction(txn)
5. unique制約
datastore上で、unique制約を作るのはちょっと大変。ユーザ情報を格納するUserモデル内でemailアドレスをuniqueにしたい、とか非常にありがちなことが簡単にはできない。
こういう場合にも、key nameがうまく使える。unique制約を保持するためのモデルを別に作って、uniqueにしたいデータ(この場合emailアドレス)をkey nameにしてしまうのがいい感じ。上記の、insert_or_fail で作成してみて、作成できたらそのemailアドレスは使える、という感じ。(何か他に定石がありそうなんだけど。)
6. データの一括更新
システムのアップグレード時など、データを一括で更新したくなるときがある。SQLだとUPDATE一発みたいな更新でも、datastoreだとちょっと大変。こういうときは map/reduce を使うと素早く簡単に全件の更新ができてよい感じ。(件数多いと、CPU時間==お金を一気に消費しちゃうけど)
(このappengine-mapreduceはmap/reduceってか、単に全レコードに対して並列で何かができるだけなんだけど。本当のmap/reduceはそのうち導入されるらしいので楽しみ。)
7. ランキング
ちょっとしたゲームっぽいものを作ると「スコアランキング」みたいなものが必要になったりするものだけど、このランキングってのはRDBでもそれなりに厄介なもので、何も考えないで作ると、大量のアクセスが発生してしまい使いものにならなくなる。その結果、バッチ処理で静的なランキングを作成するとかちょっとダサい感じになってしまいがち。
で、ちょっと自分で色々なアルゴリズムで実装してみたりしてたのだけど、便利なライブラリを見つけてしまったのでそれを使っている。 このライブラリ。
8. 複数レコードの保存/削除は db.put() / db.delete() を使って一括で行う
スピードが全然違う。db.delete() はクエリをそのまま突っ込んだりもできる。
9. 全文検索
全文検索は、現在の Google App Engine ではまともに実装されていないらしい。(Googleのくせに! 近い将来実装されると発表されたが)
ただ、簡易的には SearchableModel が使える。使い方は、 コードの中のコメント が一番詳しいような気がする。
そのコメントにあるように、かなり低機能で日本語も使えない。 ただ、とりあえずそれっぽく動かすだけなら簡単に使える。
ログ関連
10. loggingを使ってログを取る
logging moduleを使うことで、実行時に色々なログを取れる。Google App Engineともうまく統合されていて、Web上のadminインタフェースで、httpアクセス毎に参照できてとても便利。
11. POSTされたデータとそれに対応するresponse bodyをすべてログに
loggingを使ってPOSTの中身とresponseをそのままログに取っておくとデバッグ時に便利。Google App Engineの管理画面からアクセス毎にPOSTとそれに対応するresponseの内容を見ることができる。
これをするためのMiddlewareを書いた。(実際には 前回 書いたようにセッション管理用の SessionMiddleware も挟んでいる。)
class PostLoggerMiddleware(object): def __init__(self, app): self.app = app
def __call__(self, env, start_response): request = webapp.Request(env) if request.body: logging.info("request: " + str(request.body))
def my_start_response(status, response_headers, exc_info=None): write = start_response(status, response_headers, exc_info) def my_write(body): logging.info("response: " + str(body)) write(body) return my_write
return self.app(env, my_start_response)
application = PostLoggerMiddleware(webapp.WSGIApplication([ ...
12. exceptionをtrackback付きでログに
基本的に何もしなければ、Traceback付きでログに乗る。しかし、コード側で、 handle_exception を定義してexceptionをすべてcatchして処理を続けたいときもある。
たとえば、現在JSON APIをGoogle App Engine上で作っているのだけど、例外が起きた時もJSONでエラーを返すことにしている。
こういう場合には、その中で JSONを返す処理を書きつつ、logging.exception(exception) と書いておくと、admin画面上では普通にTraceback付きでエラーが見える。
その他
13. 自分のアプリを複数バージョン同時に持てる。ただし datastore は共通。
最初知ったときは、datastore共通で何に使うんだよ、と思ったんだけど色々便利なこともあった。
- 新バージョンのテスト。わりとメインの使い方か。でも、datastoreは一個なのであまり思い切ったことはできない。
- バージョン毎にログが別になる。Google App Engineのadmin画面はバージョン毎なので、ログも別々になる。map/reduceとかを同じバージョンでやるとログがそれで溢れてしまってうざいことがあるんだけど、それも別にしておくといいかもしれない。
- propertyの型を変えるとかちょっとしたmigrationをするときに別のバージョンでやるのがいい(下記参照)。
14. 非同期でいい処理にはできる限り Task Queue を使う
リアルタイムでなくていい処理は全部 Task Queue に回してしまってもいいぐらいかも。
主な理由は二つ。
- HTTPアクセスの処理を早く終わらせるため
これはもちろんユーザのために、という意味もあるが、Google App Engineの設計ではとにかく早く処理を返すことがスケール面でも有効らしいので大事。
- リトライしてくれる
Google App Engineはそれなりに不安定なので、Google App Engineの都合でエラーがそれなりの頻度で起こる。そのさい task queue に入れておけばGoogle App Engineが直るまでretryしてくれる。
特に、mail送信などのAPIはしばしばtimeout (例えばこんなエラー: “DeadlineExceededError: The API call mail.Send() took too long to respond and was cancelled”) とかするので、これは必ず task queue に入れておいたほうがよさそう。メールは元々非同期で問題ないので相性もいい。
15. Modelのpropertyの型の変更
datastore自体はどんな型でもレコード毎に柔軟に持てるのだけど、その上で動くModelクラスは型にとても厳しくなっている。floatからintegerへなどの型も自動的には変換してくれない。その上read時にexceptionを出してくれてしまうので、結構厄介。
運用を初めてから型を変更する必要がでてきた場合には、以下の方法がいいみたい。
db.Expando クラスを使って値を更新する。db.Expandoクラスは、Modelのサブクラスなんだけど、設定していないpropertyも読み書きができる。また、システムを止めずに更新したい場合には、アプリの別バージョンを作成し、その上で行うと影響をあまり与えずにできる。
例:
class HogeUser(db.Model):
foo = db.StringProperty()
bar = db.StringProperty()
score = db.FloatProperty()
このscoreをfloatからintegerに変更したくなったとする。
こういう場合には、このクラスを一時的にdb.Expandoから継承するようにして、かつscoreを削除してしまう(データ自体は消えない)。
class HogeUser(db.Expando):
foo = db.StringProperty()
bar = db.StringProperty()
# score = db.FloatProperty()
そして、以下のようなmap/reduceを走らせる。
def alter_score_to_int(hoge_user):
if "score" in hoge_user.dynamic_properties() and type(hoge_user.score) is float:
hoge_user.score = int(hoge_user.score)
hoge_user.put()
終わったら、元に戻して Int に変更する
class HogeUser(db.Model):
foo = db.StringProperty()
bar = db.StringProperty()
score = db.IntegerProperty()
16. エラーをメール通知
http://code.google.com/appengine/articles/python/recording_exceptions_with_ereporter.html
標準で入っているこのライブラリを使うと、発生したエラーをdailyでまとめてメールしてくれる。それほど高機能ではないが、同じようなエラーはまとめるという最低限なことはしてくれる。自分でメールの内容をカスタマイズもできるみたい。
こんな感じで最初の方に書いている。ローカルでのSDK環境の場合、エラーになってしまうのでスキップしている。
in_local_sdk = (os.environ.get("SERVER_SOFTWARE") == "Development/1.0") or os.environ.get("TERM")
if not in_local_sdk:
from google.appengine.ext import ereporter
ereporter.register_logger()
その他の設定は、上のリンク先のまま。
17. 死活監視
Google App Engine上のアプリの死活監視をする場合の注意点として、Google App Engineが不安定になっている場合、readはokで、writeだけがエラーになるケースがあるようなので、writeが発生するような処理で監視をしたほうがよさそう。
参考にしたところ
http://www.mail-archive.com/google-appengine@googlegroups.com/msg20008.html
http://stackoverflow.com/questions/4742875/change-integerproperty-to-floatproperty-of-existing-appengine-datastore
http://devblog.miumeet.com/2011/02/schedule-mapreduce-daily-on-appengine.html
http://googleappengine.blogspot.com/2009/06/10-things-you-probably-didnt-know-about.html
他多数、だが忘れた。
追記: 続編的なものを書いた。
今年は色々なことに手を出してみよう、ってことで少し前からGoogle App Engine(以下GAE)で、あるモノを作っている。モノ自体は近いうちに公表できると思う。
基本的に、Pythonと標準っぽいフレームワークだけでやってみている。作っているものがそれなりにシンプルなのと(だからこそGAE!)、GAEでそれなりの規模の開発をするのが自分自身初めてということもあり、あまり色々なレイヤーを重ねて手こずりたくなかった、ってのがその理由。
ただ、GAE初心者なので、「いやいやそれは今時ないよ」「XXの方が100倍いい」とかあったら教えてくれると嬉しいので今のところの環境を書いておくことにした。今ならスイッチ可能。
今作っているものがJSONファイルを入出力するだけのものなので、HTML生成パートみたいのはない。
1. フレームワーク
上にも書いたように、今回は、わりと単純なアプリケーションの作成なので、Google App Engine (Python)で提供されている最小限のフレームワークだけを使っている。
2. Pythonのバージョンは2.5
Mac (Snow Leopard) だと、2.6と2.5の両方が入っていて、デフォルト(/usr/bin/python)のバージョンは2.6。そのため普通にGoogle App Engineの開発環境をインストールすると2.6が使われてしまっていて、最初の数時間無駄にした。
defaultコマンドで /usr/bin/python の指すバージョンを変えられるようだけど、他に影響を与えたくなかったので、今回は、
- コマンドラインから実行するpython関連のコマンドは2.5を付けて実行(例: easy_install-2.5)
- GoogleAppEngineLauncherで、 /usr/bin/python2.5 を指定
で、なんとかなっている。
3. ディレクトリ構成
「GAE/Pythonスタンダードなディレクトリ構成」みたいのはないようなので、いろいろな公開されているプロジェクトを参考にして以下のような配置にしてみた。最初はもうちょっと深い構成にしていたのだけど、これぐらいで十分かも。今のところModelは1ファイルにまとめてるがだいぶ増えてきたので、近い将来分離する予定。
application dir/
models.py
handlers/foo_handler.py
handlers/bar_handler.py
tests/test_foo_handler.py
tests/bar_foo_handler.py
main.py
app.yaml
controller的なものをhandlerと呼ぶみたいなのでそうしてる。
4. テスティング環境
もう当然TDDだよね、ということで最初に手をつけたのがテスト環境の構築。
まず、ここしばらくWeb開発でRuby on Rails + rspecメインな生活をしていることもあり、GAE/Python上でもそれっぽいのを探してみたところ、いくつかそれっぽいのはあったのだけど、「開発が活発ではない」「使いづらそう」という感じがしたのでその方向は諦めて、普通にUnitTestでいくことにした。マイナーなものを使うよりも、自分以外の人が触ることになったときのことを考えて、ふつーなものにしておいたほうがいいかな、という判断もある。
素のUnitTestだけだといろいろいまいちなので、以下の物を入れた。
sudo easy_install-2.5 nose
sudo easy_install-2.5 nosegae
sudo easy_install-2.5 gaetestbed
sudo easy_install-2.5 webtest
使い方等は、その中のgaetestbedの README 読めばなんとなく使い方はわかるので省略。
5. doctest併用
UnitTestの他に、コードの中にテストを書ける doctest も使っている。基本的にhandlerのテストはUnitTestで、modelのテストはdoctestで書いている。
併用している理由としては、長い間うっすらと「なんかdoctestいいなー、いつか使ってみたい」と思っていたのと、今回はそれほど複雑なmodelがないというのがある。
nosegaeは、UnitTestとdoctestの両方を同時に扱えるので、この二つを混ぜても素直に全体のテストの実行が1コマンドできる。オプションに—with-doctest を付けて、
> nosetests --with-gae --with-doctest
こんな感じ。CIでもほぼこれと同じコマンドを実行している。
6. セッション管理
GAEでは、Googleアカウントと連動したセッション+アカウント管理は標準で提供されているようだけど、今回は自前でセッション/アカウント管理をしたかったので、それは使わずに、セッション管理には gae-sessions を使ってみた。他にも色々あるのだけど、単純で軽そうだったのでこれにした。
そのライブラリの作者が作った他のライブラリとの 比較表 が参考になる。完全勝利になってるのは若干あやしいが。
このライブラリで作成したセッションに、自分で作ったユーザ用のモデルを入れるという簡単実装にした(gae-sessionsのセッションの実装自体は普通にCookieを使ったものなのでデバッグ等も簡単)。
一つ躓いた点として、gae-sessionsの導入手順をREADMEに従うと、 appengine_config.py を作ってそこでmiddlewareをセットする、となっている。ただ、ローカルの開発環境でnosegaeを使いunit testを流すと、 appengine_config.py は読んでくれないので、main.py の中で直接読み込んでごまかしてる。こんな感じ。
application = SessionMiddleware(
webapp.WSGIApplication([....],
debug=True),
cookie_key="hogehogehogehogehogehogehogehogehogehogehogehogehogehoge")
7. 本: Code in the Cloud
とりあえずGoogle App Engineを始めるにあたって何冊か読んだ本の中でそれなりに良かった奴。JavaとPython両方が書いてあるのが微妙に読みづらいが、最初に読む本、としてはそこそこいい本だと思う。テスト関係のことはほとんど載ってないけど。
8. まとめ
以上の環境で、ぼちぼちコードを書いているけど、気持ち良くTDDができている。GAE自体は思ってたよりも(話に聞いていたよりは)複雑でも難しくもなく素直だなーって印象。ドキュメントもわかりやすい。まあ、後発組は楽だなあ、と。
とは言え、もうちょっとやると壁にあたりそうなので、そのときまた何か書きたいと思う。
9. 参考にしたもの
http://www.cuberick.com/2008/11/unit-test-your-google-app-engine-models.html
https://github.com/jgeewax/gaetestbed/
http://www.slideshare.net/adeoshineye/test-driven-development-on-google-app-engine