Google App Engineであそんでみよう.(5)

Datastore

Google App Engineのハイライトであるデータベースとの連携API. 世間でよくつかわれているデータベースはMySQLにしろPostgreSQLにしろRelational Database(RDB)と呼ばれるものの一種であり, SQLという標準化された問い合わせ言語でアクセスすることができる. SQLは非常に強力で, それが故に普及している訳だが, 実装のコストは高く, とくに分散実装が非常に難しい. Google App EngineではRDBのある意味で不必要なほど強力な機能の一部をあきらめる代わりに, より分散実装しやすいデータベースモデルを提供している.

エンティティ

Datastore のデータモデルはカード型データベースに近いがそれよりもさらにゆるい. 登録される個々のデータの固まりをエンティティと呼ぶ. エンティティにはKind(種)という概念がある. データのクラスのようなものだと思うのだが, 同じKindでも異なるプロパティを持つことが許されるようだ. まあ, Python とか Ruby とかのスクリプト言語のオブジェクトインスタンスも同じような仕様だからあまり不思議は無いのかもしれない.

個々のエンティティはユニークなIDを持つ. このIDはエンティティをDatastoreにputした際に自動的に与えられる.

エンティティのKindはPythonのクラスとして定義する.このとき2つの方法がある. ひとつはdb.Modelを継承する方法. もうひとつはdb.Expandoを継承する方法. db.Modelのほうは, プロパティが静的にさだまったエンティティを宣言するのに用いる. db.Expandoはプロパティが動的に変化するものを宣言するのに用いるようだ. 後者の使い道はわたしにはよくわからない. Modelを用いたKindの宣言はこんな感じ.

from google.appengine.ext import db

class Memo(db.Model):
  author = db.UserProperty()
  content = db.StringProperty()
  date = db.DateTimeProperty(auto_now_add=True)

MemoというKindには「書いた人」と「メモの中身」と「書いた日」というプロパティがある, という宣言になっている. 「書いた人」がUserPropertyになっていて便利. ここにはusers.get_current_user()で取得したUserクラスのオブジェクトをそのまま入れられる. 間違って文字列を入れたらエラーが出た.
エントリを作ってデータベースに書き込むには, このオブジェクトを作って, putしてやればよい.

memo = Memo()
memo.author  = users.get_current_user()
memo.content = "memo memo"
memo.put()

すんごい簡単. データベースへの接続という概念が存在しないのが画期的だ. もちろん動作環境を厳しく限定しているからできる技なわけだが.

でも, よく考えてみると背後で何がおこっているのかよくわからない. memo.content = "memo memo" とやると, 普通に考えれば, memoオブジェクトのインスタンス変数contentが上書きされるはずだが, もともと入っているdb.StringProperty()はどうなっちゃうんだろうか.

ソースを追ってみると, StringProperty クラスのスーパークラスであるPropertyクラスに__set__メソッドが定義されていて, そこが呼ばれているようだ. 要するに, StringProperty() が上書きされるのではなく, このインスタンスの中にセットした値が書き込まれているということだ. なるほど.

エンティティの読み出し

データベースに書き込まれたエンティティの読み出しにもいくつも方法がある. 一番簡単なのは, idを指定して読みにいく方法.

memo = Memo.get(ID)

簡単だが, 普通は読みたいオブジェクトのIDが事前に分かっているということはないのであまり使えない.

特定のKindのすべてのエンティティを取得するには all() を用いる.

for memo in Memo.all():
    self.response.out.write('memo id = ' + memo.to_xml())

なにがおこっているのか分かりにくいが, Memo.all() では後述するQuery オブジェクトが生成されている. このQuery オブジェクトがiterable になっているのでこういう書き方ができる. 明示的なfetchを省略しないで書くとこうなる. fetchの第一引数は取得するデータの上限なのだが, 省略できないので面倒.

for memo in Memo.all().fetch(3):
    self.response.out.write('memo id = ' + memo.to_xml())

ソースを追って見ないとわからないが, ひょっとするとiterableとして使う場合とfetch()でとってくる場合では内部の動作が違うのかもしれない. 前者はJDBCのCursorのようなものを使っていて, すべての返答を一度に取り出してはいないのかもしれない.

Query オブジェクト

Query オブジェクトはデータベースに対するQueryを抽象したオブジェクトで, 条件をつけていくことで対象をしぼりこんだり, 出力する順番を指定することができる. 基本的には前述のall()メソッドを使って「すべてを取り出す」というQueryを作っておいて, filterメソッドで対象を絞り込んでいく. all() メソッドの代わりにQueryオブジェクトのコンストラクタにKindをわたしてもよい.

q = Memo.all()
# 上としたは等価
q = db.Query(Memo)

filter メソッドには, プロパティに対する条件を文字列で書く.

Memo.all().filter('user = ', users.get_current_user())

とやると, そのユーザが書いたメモだけを取り出すことができる. filterメソッドの第一引数は

プロパティ名, スペース, 比較演算子

となっている. 演算子としては, < <= = >= > がサポートされているが, != がない. これはかなり不便かも.

取り出す順番はorderメソッドで操作することができる.

Memo.all().order('date')

とやると, メモが書かれた順番で取り出すことができる. 逆順にするにはプロパティ名の前に, マイナスをつける.

Memo.all().order('-date')
エンティティグループ

エンティティグループという概念がある. エンティティを作成する際に, 親エンティティを指定することができる. この親子関係で接続された固まりをエンティティグループと呼ぶ. なんで, エンティティグループなどというがいねんがあるかというと, トランザクションのため. Datastoreでは, 一つのエンティティグループに対してしか, アトミックなトランザクションが実行できないのだ. なんか普通に書くと, 念のためすべてのエンティティを同じエンティティグループにいれてしまいたくなりそうだが, そうするとたぶんスケーラビリティがでなくなってしまうのだろう. 実際ドキュメントにも, トランザクションが必要ないなら, グループを使うな, と書いてある.