DataStore
DataStoreとは
本記事内のDataStoreは、GoogleAppEngineのデータストアのことを指す。GoogleAppEngineは、ファイルの書き込みが一切できないため、DataStoreを使う以外にデータを永続化する方法はない。短期であればmemcachedに保存することもできるが、memcachedに保存されたデータは保存期間が保証されないため、一時データをキャッシュして高速化(or 負荷軽減)以外には実質つかえない。
DataStoreの特徴
DataStoreの特徴は、スケーラブルであること。データ数が1万件でも1億件でもほぼ同じ時間で結果が返ってくる(らしい)。ただし、通常のRDBでは簡単にできる操作がDataStoreでは非常に重い処理だったり、不可能だったりするので注意。
苦手な処理
- 件数のカウント
⇒件数のカウントは、データ全体を取ってくる処理に近いだけの処理時間が掛かります。
※python-blog-systemでは、1ページ当たりの件数+1を取得して、次のページがあるかどうか判定してます。 - 後方にあるデータの取得
⇒MySQLなどでは、limit 1000,10などと、気軽にかいてしまいますが、DataStoreでは注意が必要です。インデックスが用意されていない項目は1000件目までしか取り出せません。また、インデックスが付いていても、offsetに10000などの、大きな数字を設定するとレスポンスが悪化します。
出来ない操作
- Joinできない
ReferencePropertyを使うとデータ間の関連を表現できます。リレーションを実現するためには、あらかじめModelにReferencePropertyを設定する必要があります。 - 複数のデータに対してDelete、Updateが使えない
更新するデータ、削除するデータを取り出して、1つずつ更新(put)もしくは削除(delete)するしかない。 - auto_incrementフィールドを作れない
いわゆる「連番フィールド」を作れません。欲しければ、「GAE/Python で auto_increment」などを参考に実装してください。 - 部分属性だけを取り出せない
RDBのように、属性を指定して取り出すことはできません。必ず SELET * FROM になります。
db.Modelとdb.Expandoとpolymodel.PolyModel
DataStoreには、以下の3つのクラスを継承したクラスのインスタンスを保存できます。
- db.Model
あらかじめ定義されたプロパティ(固定プロパティ)しか保存できない。 - db.Expando
事前に定義されていないプロパティ(動的プロパティ)も保存できる。 - polymodel.PolyModel
モデル間の継承関係を実現できる。動的プロパティには対応しない。本記事では省略。
使用例
db.Modelを使ったBBSプログラムの例を示す。自動生成されるテンプレートに以下の変更を加えた。
- 5行目
dbモジュールをインポートした。これでdb.Modelやdb.Expandoを利用できる。 - 7行目~8行目
DataStoreに保存されているMessageクラスのデータをすべて取り出し表示。 - 9行目
メッセージ入力用のHTMLフォームと、送信ボタンを表示。 - 14行目
postで受け取ったデータをDataStoreに保存する。getで渡されたデータも同じ方法で取得できる。 - 15行目
元のURIにリダイレクトする。 - 22行目~23行目
DataStoreに保存するオブジェクトを定義。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #!/usr/bin/env python</p> from google.appengine.ext import webapp from google.appengine.ext.webapp import util from google.appengine.ext import db class MainHandler(webapp.RequestHandler): def get(self): for message in Message.all(): self.response.out.write(" <div>%s</div> " % message.msg) self.response.out.write(' <form method="post"><input type="hidden" name="phpMyAdmin" value="cfc2644bd9c947213a0141747c2608b0" /><input name="msg" /><input type="submit" /></form>') def post(self): Message(msg = self.request.get('msg')).put() self.redirect('/') def main(): application = webapp.WSGIApplication([('/', MainHandler)], debug=True) util.run_wsgi_app(application) class Message(db.Model): msg = db.StringProperty() if __name__ == '__main__': main() |
実行画面
Relation
冒頭で述べたように、DataStoreではJOIN演算を使えない。Model同士を結びつけるには、あらかじめ、Model自体にReferencePropertyもしくはSelfReferencePropertyを定義しておく。
- db.ReferenceProperty
他のモデルへの参照。モデルA⇒モデルBの参照を作ると、モデルB⇒モデルAへの逆参照も自動生成される。 - db.SelfReferenceProperty
同一モデルへの参照。
1:nのリレーション
1対nのリレーションを使った例を示す。先ほどの例との主な差分は以下の通り。
- 10行目~13行目
コメントの一覧表示と、コメント投稿用のHTMLフォームの表示 - 18行目~21行目
コメントをDataStoreに保存。22行目で、親となるMessageモデルのインスタンスを設定している。 - 31行目~33行目
Commentモデルの定義。collection_nameを使って、親モデルから参照できる。10行目参照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #!/usr/bin/env python from google.appengine.ext import webapp from google.appengine.ext.webapp import util from google.appengine.ext import db class MainHandler(webapp.RequestHandler): def get(self, key): for message in Message.all(): self.response.out.write("<div>%s</div>" % message.msg) for comment in message.comments: self.response.out.write("<div> - %s</div>" % comment.cmt) self.response.out.write('<form method="post" action="/%s"><input type="hidden" name="phpMyAdmin" value="cfc2644bd9c947213a0141747c2608b0" /><input name="cmt"><input type="submit" value="cmt"></form>' % message.key()) self.response.out.write('<form method="post"><input type="hidden" name="phpMyAdmin" value="cfc2644bd9c947213a0141747c2608b0" /><input name="msg"><input type="submit" value="msg"></form>') def post(self, key): if self.request.get('msg'): Message(msg = self.request.get('msg')).put() elif self.request.get('cmt'): Comment( cmt = self.request.get('cmt'), msg = Message.get(key) ).put() self.redirect('/') def main(): application = webapp.WSGIApplication([('/(.*)', MainHandler)], debug=True) util.run_wsgi_app(application) class Message(db.Model): msg = db.StringProperty() class Comment(db.Model): cmt = db.StringProperty() msg = db.ReferenceProperty(Message, collection_name = 'comments') if __name__ == '__main__': main() |
n:nのリレーション
n:nのリレーションはデータモデルの部分の例だけ紹介する。
以下のソースコードpython-blog-systemで実際に使っているモデルだ。ブログの記事と、タグは多対多の関係になる。
1 2 3 4 5 6 7 8 9 10 | class Entry(db.Model): title = db.StringProperty(default = "") body = db.TextProperty(default = "") tags = db.ListProperty(db.Key) #タグのリストを保持 datetime = db.DateTimeProperty(auto_now_add = True) class Tag(db.Model): tag = db.StringProperty() @property def entries(self): #実体を持つのではなく、毎回クエリを実行する。 return Entry.all().filter('tags', self.key()).order('-datetime') |
1:1のリレーション
1:1のリレーションは、1:nのリレーションの特殊な形なので、モデルの部分だけ紹介します。GAEでは、モデル内の特定フィールドだけを取り出せないため、頻繁に取り出すモデルから重いデータは除外しておきましょう。
重いModel: 必ず写真をロードしてしまう。
1 2 3 | class Person(db.Model): name = db.StringProperty() picture = db.BlobProperty() #重いデータ |
分割例: 写真は必要になった時にロードすればよい。
1 2 3 4 5 6 | class Person(db.Model): #Personは軽い name = db.StringProperty() class Picture(db.Model): #写真は必要なときだけ person = db.ReferenceProperty() picture = db.BlobProperty() |
トランザクション
GAE/Python で auto_incrementで、トランザクションを使ったシンプルなプログラムを紹介している。この例では、1つのエンティティに対する読み書きをトランザクション内で実行している。複数のエンティティに対するしょりを、トランザクション内で実行するためには、あらかじめエンティティ同士を関連付けておく必要がある。詳細はトランザクションをご覧ください。