Sinatra on GAE/JRuby でデータを永続化する (準備編)
前回の日記「10分ではじめる GAE/JRuby」の最後に、以下のように書いた。
GAE を使うならデータストアは避けて通れない。これも以前なら Java の API を叩いていたみたいだけど、今なら appengine-jruby ライブラリから呼び出す方が楽みたい。 DatastoreOverview を一読するのがよさそう。以下のようなサンプルコードが書かれている。
という訳で、今回は GAE/JRuby 上でのデータの永続化に挑戦する。 正直なところ手探りでの挑戦だったので、回り道をしているかもしれないし、間違いを書いているかもしれない。 でも、調べることにも意味があると思うので、僕がたどったプロセスをなるべくそのまま残してみる。
ドキュメントをざっと読む
GAE/JRuby でのデータ永続化については、まだまだ Web 上にドキュメントが少ない状態。 こういう場合は、最初に公式ドキュメントを読む方が結果的に近道になる。
- GAE/JRuby (appengine-jruby) のドキュメント (英語)
- GAE/Java のドキュメント (日本語)
あわせて、 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 を試してみよう、ということで。
長くなってきたので、続きは次の日の日記に書くよ。