at posts/single.html

Sinatra on GAE/JRuby でデータを永続化する (Datastore編)

準備が整ったので、ようやく Datastore での CRUD 操作に挑戦する。 ここからは API のドキュメントが道しるべになる。

サンプルアプリ(歩数管理ソフト)

できあがったサンプルは以下の通り。 数日後に消すかもしれない。

GAE/JRuby sample app (using Datastore)

日付や歩数に任意の文字列が入れられたりと、細かいところまでは作っていない。 でも、データの CRUD と検索の概要は分かると思う。

Datastore の概要を把握する (get, putとキーの関係)

ふたたびサンプルコードに目を通す。 データの永続化のために、 Datastore.put メソッドを呼んでいる。

require 'appengine-apis/datastore'

e = AppEngine::Datastore::Entity.new('Employee')
e[:name] = 'Fred'
e[:role] = 'Manager'
e['hire_date'] = Time.now

AppEngine::Datastore.put e

API を読むと Datastore.get メソッドも存在する。 get では引数に key を渡してエンティティを取得している。 この key には、何を指定すればいいんだろう。

ここで行き詰まったので、立ち止まって Datastore の概念を調べてみる。

Datastoreのエンティティテーブルとは

  • Datastoreのエンティティを保存するBigtableのテーブル
  • 個々のエンティティは、「エンティティキー」で識別される
  • 個々のエンティティのプロパティ内容は、すべて1つのカラムにシリアライズされて格納される

なるほど、 Bigtable というフラットで巨大な key-value ストアの上に、 Datastore というエンティティテーブルが存在するのか。 そして、「1つのカラムにシリアライズされて」というところがポイント。 細かいところまでは分からないけど、イメージだけはつかむ。

key に何が入るのかを試してみるために、以下のサンプルを作ってみる。 dateが日付 ("2009-09-06"のような文字列) で steps が歩数。 日付をキーとして、 Datastore に格納している。

# create new record
post '/record/' do
  @record = AppEngine::Datastore::Entity.new('Record', params[:date])
  @record[:date] = params[:date]
  @record[:steps] = params[:steps]
  new_key = AppEngine::Datastore.put(@record)   # new_key == record.key

Entity.newの第1引数 ('Record') がエンティティ名で、第2引数がエンティティを識別するための ID になる。 Datastore に格納するためのキーは、エンティティ名とIDから生成しているみたい。 キーは Entity オブジェクトの key メソッドでも取り出せるし、 Datastore.put の返り値にもなる。 Entity.new の時点で ID を空にした場合は、 Datastore.put の時点で自動的に ID が振られる模様。 キーについては、以下のドキュメントも参考になる。 Java のドキュメントは JDO の概念が混じっているので、少し混乱するけど。

Datastore からエンティティを取得するには、この key を使う。 key はエンティティ名とIDから生成できるので、日付をキーにして Datastore に保存した歩数を取り出すコードは以下のようになる。

# get one record
get '/record/:date' do
  # エンティティ名と日付からキーを生成
  @key = AppEngine::Datastore::Key.from_path('Record', params[:date])
  begin
    # キーから歩数データを取得する
    @record = AppEngine::Datastore.get(@key)
  rescue AppEngine::Datastore::EntityNotFound => e
    @record = {}
  end
  erb %{
    date: <%=h @record[:date] %><br />
    key: <%=h @key %><br />
    steps: <%=h @record[:steps] %><br />
  }
end

例えば、 Entity.new('Record', '2009-09-06')で生成した key は、値が "agttYWNodS1qcnVieXIWCxIGUmVjb3JkIgoyMDA5LTA5LTA2DA" となった。 ちなみに、 key の型は Java::ComGoogleAppengineApiDatastore::Key なので、ただの文字列を Datastore.put に渡してもうまく行かない。 Key.from_path や Entity#key メソッドの返り値を使うこと。

同じように delete も試してみた。

# delete one record
delete '/record/:date' do
  @key = AppEngine::Datastore::Key.from_path('Record', params[:date])
  @record = AppEngine::Datastore.delete(@key)

これで動きそうなはずだけど、なぜか「no delete with arguments matching」というエラーが発生する。 とりあえず delete は後回し。

ここまでで、歩数管理ソフトの CRUD 操作ができた(Deleteだけ動いていないけど)。

Datastore でクエリを使う

キー(日付)を使った歩数データの出し入れができたけど、わざわざ日付を入力して歩数データを参照するのはちょっと面倒。 そこで、歩数データの一覧を取得してみる。

一覧を取得するにはクエリという機能を使う。 まずは、何も条件を指定せずに、歩数データの一覧を取得する。

# get all records
get '/record/' do
  query = AppEngine::Datastore::Query.new('Record')
  @records = query.fetch

@records は Datastore::Entity の配列(のようなもの)。 @records.each とすると、それぞれのエントリが取得できる。 (実際には、eachメソッドを呼んだ時点で検索が実行されるみたい)

条件を指定する場合は、fetchではなくfilterを使う。 以下のコードは、指定した歩数以上のデータを取得する例になる。

query = AppEngine::Datastore::Query.new('Record')
@records = query.filter('steps', AppEngine::Datastore::Query::GREATER_THAN_OR_EQUAL, params[:steps])

ここまでのまとめ

分かったこと。

  • Datastore はキーを使ってオブジェクトを出し入れする。
  • キーはエンティティ名とIDから一意に生成される。
  • クエリを使うことで複数のエンティティを検索・取得できる。

まだ分かっていないこと。

  • Datastore のクエリに指定できる条件 (RDBと違って検索条件は限られる)
  • 関連を含むエンティティの扱い方

次は DataMapper を使っての CRUD か、もしくは Datastore での関連を試してみよう。

サンプルコード