at posts/single.html

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

前回の日記「10分ではじめる GAE/JRuby」の最後に、以下のように書いた。

GAE を使うならデータストアは避けて通れない。これも以前なら Java の API を叩いていたみたいだけど、今なら appengine-jruby ライブラリから呼び出す方が楽みたい。 DatastoreOverview を一読するのがよさそう。以下のようなサンプルコードが書かれている。

という訳で、今回は GAE/JRuby 上でのデータの永続化に挑戦する。 正直なところ手探りでの挑戦だったので、回り道をしているかもしれないし、間違いを書いているかもしれない。 でも、調べることにも意味があると思うので、僕がたどったプロセスをなるべくそのまま残してみる。

ドキュメントをざっと読む

GAE/JRuby でのデータ永続化については、まだまだ Web 上にドキュメントが少ない状態。 こういう場合は、最初に公式ドキュメントを読む方が結果的に近道になる。

あわせて、 WEB+DB PRESS Vol.51の Google App Engine for Java 特集を手元に用意した。

「AppEngine::Datastore」 と 「DataMapper::Adapter::AppEngineAdapter」の2つのライブラリ

まず、 GAE/JRuby のドキュメントとサンプルコードを読んで、全体像をつかもうとする。 Getting Started にはゲストブックのサンプルコードが書かれている。 一部だけを以下に抜粋する。

require 'dm-core'

# Configure DataMapper to use the App Engine datastore 
DataMapper.setup(:default, "appengine://auto")

# Create your model class
class Shout
  include DataMapper::Resource
  
  property :id, Serial
  property :message, Text
end

post '/' do
  # Create a new shout and redirect back to the list.
  shout = Shout.create(:message => params[:message])
  redirect '/'
end

ざっと読んで気がついたこと。Java っぽくもあり、 Rails っぽくもある。

  • 'dm-core' ライブラリを読み込み、 DataMapper というクラスをインクルードしている。
  • Shout というクラスにて属性とその型を定義している。
  • Shout モデルに対して create メソッドを呼ぶことで、データを永続化している。

もう一つのドキュメントである Datastore Overview にも目を通す。

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
  • 'appengine-apis/datastore' ライブラリを読み込み、 AppEngine::Datastore::Entity クラスを生成している。
  • 専用のモデルクラスは用意せずに、ハッシュのキーと値で属性を表現している。
  • Ruby の PStore っぽいインタフェース。

2つのドキュメントを読んで、 DataMapper と Datastore という2つのインタフェースが用意されていることに気がつく。 でも、両者の違いが分からない。 そこで、 DataMapper で検索してみると、Ruby Freaks Lounge - 第14回 DataMapperの使い方というページが見つかった。

前回に続いて,今回はMerbで主要なORMとして使われているDataMapperを紹介します。 (中略) 例えば,DataMapperでCouchDB を使うためのアダプタ(dm-couchdb-adapter),Google App EngineのDataStoreを利用するアダプタ(dm-datastore-adapter),REST APIを公開するWebサービスをストレージとして利用するアダプタ(dm-rest-adapter)などが存在しています。

DataMapper は Merb 版の ActiveDirectory のようなもの (O/Rマッパー) か。 ということは、クラス名から推測すると AppEngine::Datastore が GAE/Java で用意しているストレージのラッパーだろう。 そして、 AppEngine::Datastor の上に DataMapper 対応のライブラリが用意されているのではないか。 そう考えて、 DataMapper::Adapter::AppEngineAdapter のソースを読むと、確かに内部で AppEngine::Datastore を呼び出していた。 さらに、どちらのライブラリの作者も GoogleのRyan Brownさんということが分かった。

サンプルアプリを考える

ここで、 Datastore と DataMapper のどちらを使おうか、ちょっと考える。 おそらく、 DataMapper の方が抽象化されているので使い方は楽だろう。 でも、元々決められている DataMapper の標準的なインタフェースを、 GAE の Datastore が全てサポートしているとは限らない。 なので、 Datastore → DataMapper の順で簡単な CRUD アプリを作ってみることにした。

作るアプリは、以下の理由から「みんなで使える歩数記録ソフト」に選定。

  • 単純な1エンティティの CRUD から、よくある 1:N の関連づけへと発展できる。
    • まずは1人で使うことを想定して、日付と歩数のペアを1つのレコードとして管理する(1エンティティのCRUD)。
    • 次に、みんなで使えるように、ユーザとレコードを関連づけて管理する (1:Nの関連づけ) 。
  • 日経ソフトウエア2009年2月号の「SQL練習問題」でも取り上げたお題なので、RDBとの使い方の比較もできる。

Datastore を使うための準備と謎の NameError

先ほどのサンプルコードでは、 Datastore を使う前に以下のライブラリをロードしていた。

require 'appengine-apis/datastore'

appengine-apis はまだインストールされていなかったので、 appcfg.rb gem でローカルにインストールした。

$ appcfg.rb gem install appengine-apis

この状態でサンプルコードを動かそうとしたら、以下の例外が発生した。

$ dev_appserver.rb .
=> Booting DevAppServer
=> Press Ctrl-C to shutdown server
=> Generating configuration files
/opt/local/lib/ruby/gems/1.8/gems/appengine-apis-0.0.9/lib/appengine-apis/datastore_types.rb:168:in `const_missing': cannot load Java class com.google.appengine.api.datastore.Rating (NameError)

エラーメッセージ (datastore NameError) で検索しても、直接的な原因は見つからない。 以下のサイトから、原因を推測する。

Ruby 側から Java のクラスが読めていないということは、 GAE/Java の jar がロードされていない? こういう時に、レイヤが多いと解析が大変だなぁ…。 1時間くらい悩んだあげく、ふとしたことで解決した。 それは、 google-appengine の gem を再インストールしようとしたこと。

$ gem list
appengine-sdk (1.2.2, 1.2.5)

あれ、 sdk のバージョンが 1.2.2 から 1.2.5 にあがっている? と言うわけで、原因はバージョンの不一致だった。

  • 2日前にインストールした google-appengine 本体はバージョンが 1.2.0 だった。
  • その後、 google-appengine が 1.2.5 にバージョンアップ(まだアップデートしていない)。
  • 1.2.5 対応の appengine-apis をローカルにインストール。
  • appengine-api (1.2.5対応) が google-appengine (1.2.0) に存在しない新しいクラスを呼んでいたためエラーが発生。

運が悪かったなぁ。教訓としては、 NameError の場合は gem update を試してみよう、ということで。

長くなってきたので、続きは次の日の日記に書くよ。

関連する日記