WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

ActiveResource の使い方(前編) : Rails 同士で通信する

少し前にActiveResource で Google Spreadsheets をアクセスするライブラリを公開しましたが、思ったほどブクマとかされなくて、ちょっとションボリ(´・ω・`)な感じでした。まあ、ライブラリがイマイチと言われればそれまでなのですが、それ以前に ActiveResource 自体があまりよく知られていたいのかな、という気もします。たしかに、 Google で検索しても、包括的な使い方の解説記事というのは見当たらないようです。

これではちょっと寂しいので、今回から三回シリーズ(予定)で ActiveResource の使い方をご紹介することにしました。需要があるかどうかはわかりませんが、私の中では超ホットな技術なので、かまわず突き進みますよ!(笑)

今回は初回ということで、実際に 2 つの Rails アプリケーションを作成し、 ActiveResource を使って片方のアプリケーションから別のアプリケーションのリソースにアクセスする様子を見ていこうと思います。その後、 ActiveResource の大まかな仕組みや、便利な機能などをご紹介していますので、少しでも興味がありましたら、ぜひご覧ください。

ActiveResource とは

ActiveResourceRuby on Rails 2.0 から追加された機能で、 Web 上の RESTful API を ActiveRecord のモデルと同じようなインターフェースで利用可能にする、いわば Object / RESTful API マッパーです。とくに同じ Rails アプリケーションで提供される RESTful API に最適化して設計されており、 Rails の作法に沿って作られた API であれば、僅か数行のコーディングで利用できます。その他の特徴は以下のとおりです。

  • Rails 上では ActiveRecord 同様にデータモデルとして扱える。
  • ActiveRecord と互換性の高いインターフェース。
  • ActiveRecordValidation エラーを取得可能。
  • BASIC 認証を標準サポート。
  • 主に自動テスト用に、 HTTP リクエストのモックアップをサポート。
  • 多少手間はかかるものの、既存の REST 系 API の呼び出しにも利用可能。

ActiveRecord との連携を中心に、かなり画期的なコンポーネントになっていると思います。まさに WebOS 時代の REST API クライアントライブラリですね!

使ってみよう

それでは、実際に ActiveResource を使ってみましょう。ここでは例として、 Scaffold で 2 つの Rails アプリケーションを作成し、 ActiveResource 使った通信を実装します。片方の Rails アプリケーションはバックエンドとなる API サーバーで、 API の呼び出しに応じてブックマークの情報を自分の DB に保存し、またそれを呼び出し側に提供します。もう片方は API のクライアントとなるフロントエンドの Web アプリケーションサーバーで、ユーザーがブラウザで操作できる UI を提供し、その指示によって API を呼び出して情報を保存・取得します。

構成を図にすると以下のような感じでしょうか。・・・ええ、 Lovely Charts を使ってみたかっただけですとも(^^;

サーバー・クライアントという呼び方はちょっと紛らわしいので、以降では API 提供側の Rails アプリケーションを「プロバイダ」、呼び出し側を「コンシューマ」と呼ぶことにします。上の図ではそれぞれ別のマシンのようになっていますが、実際には同じマシン上で試しました。なお、 Rails はバージョン 2.2.2 を前提にしています。

プロバイダの準備

まずはプロバイダのアプリケーションを作成しましょう。普通に rails コマンドを実行し、 Scaffold で MVC をでっち上げれば OK です。作成したら、 3000 番ポートで起動しておきます。

rails provider
cd provider
./script/generate scaffold bookmark title:string url:string comment:text
rake db:migrate
mongrel_rails start -d -p 3000

コンシューマの準備

次はコンシューマです。こちらも Scaffold を使って MVC を作成しますが、 DB は使わないので migration は削除しておきます。

rails consumer
cd consumer
./script/generate scaffold bookmark title:string url:string comment:text
rm -f db/migrate/*

そして、モデルクラス (consumer/app/models/bookmark.rb) を以下のように書き換えます。

class Bookmark < ActiveResource::Base
  self.site = 'http://localhost:3000/'
 
  alias_method :new_record?, :new?
  def initialize(attr={})
    super({'title' => nil, 'url' => nil, 'comment' => nil}.update(attr))
  end
  def update_attributes(attr={})
    self.attributes = attr
    save
  end
end

基底クラスが ActiveResource::Base になっているのに注目してください。これによって Bookmark クラスは(ActiveRecord ではなく) ActiveResource のモデルになり、 DB の代わりに self.site で指定されたサイトの Web API にアクセスして、データの取得・保存を行います。

その下では、 Scaffold のコントローラ・ビューのコードがそのままで動くように、不足しているメソッドをいくつか定義しています。基本的なインターフェースは ActiveRecord と同じなのですが、まだ細かい部分では相違点がけっこうあります。今回はサンプルということでモデル側で一括対応しましたが、本来なら呼び出し側を ActiveResource の仕様に合わせるほうが妥当でしょう。

さて、これで準備ができたので、こちらは 3001 番で起動しておきます。

mongrel_rails start -d -p 3001

以上のサンプルの全ソース(これ以降でご紹介するコードも一部含まれています)はこちらにありますので、ご利用ください。

動かしてみる

それでは、さっそくブラウザで "http://localhost:3001/bookmarks" にアクセスして、いくつかブックマークを追加してみましょう。

正常に追加できましたね。実際には、これらは Web API 経由でプロバイダ側に保存されていますので、プロバイダのページにアクセスしても同じ内容が表示されます(ポート番号に注目)。

なんと、たったこれだけで 2 つのアプリケーション間の通信が実現できてしまいました。この手軽さは Rails ならではですね。今回はプロバイダ・コンシューマともにまったく同じ機能なのでメリットが薄い(というか無い)ですが、例えば次にポータルアプリケーションを作ったとして、その中でも同じブックマークが表示・編集できたら便利だと思いませんか! ActiveResource を使えば、そのようなサーバーを跨いだ連携が簡単に実現できるわけです。

それにしても、なぜこれだけのコードで高度な連携が実現できるのでしょうか。次は、その仕組みを簡単にご紹介したいと思います。

動作の仕組み

ActiveResource の仕組みを読み解くには、まず Rails が持っている RESTful API のサポート機能を理解する必要があります。そこで、以降では Rails の RESTful ルーティングとレスポンス・フォーマットの切り替え機能を簡単にご紹介して、その後に ActiveResource の動作に迫りたいと思います。

Rails の RESTful API サポートについては既にご存知の方も多いかと思います。その場合は「ActiveResource の仕組み」まで読み飛ばしてしまってください。

RESTful ルーティング

Rails 2.0 以降で Scaffold を生成すると、 "config/routes.rb" に以下のような一行が追加されます。

map.resources :bookmarks

これがいわゆる「RESTful ルーティング」というやつで、この一行で以下のルーティングが設定されます(new, edit は API には関係ないので省略)。

パス メソッド アクション 期待される機能
/bookmarks GET index リソースの列挙
/bookmarks POST create リソースの作成
/bookmarks/:id GET show 指定リソースの取得
/bookmarks/:id PUT update 指定リソースの更新
/bookmarks/:id DELETE delete 指定リソースの削除

ポイントは、同じ URL でも HTTP メソッドが違えば別のアクションが呼び出され、別の機能が実行されるという点です。例えば、普通にブラウザから "/bookmarks" に(GET メソッドで)アクセスすると、ブックマークのリストが表示されます。しかし、適切なフィールドを持ったフォームを "/bookmarks" に POST すると、今度は新しいブックマークが作成されます。

なぜわざわざこんな小細工をするかというと、 URL というのはそもそもリソースの場所を表すもので、それに対する操作は HTTP メソッドで指定するべきだ、という REST の思想を実現するためです。しかしまあ、それはさておき、 ActiveResource 的に重要なことは、これによって「あるリソースへのアクセスが単一の URL で行え、しかもその URL が一定の規則を持っている」点です。

例えば "/bookmarks/1" に GET リクエストを投げれば 1 番のブックマークのデータが取得できるでしょうし、同じ URL に DELETE リクエストを送ればそれが削除できるでしょう。 RESTful ルーティングを前提とすることで、 ActiveResource は暗黙的に API のエントリポイントを予測し、リソースの取得、作成、更新、削除といった操作を極めてシンプルに実現できるのです。

レスポンス・フォーマットの切り替え

同じく Scaffold を生成すると、コントローラの各メソッドの最後に以下のようなコードが付加されているはずです。

respond_to do |format|
  format.html # index.html.erb
  format.xml  { render :xml => @bookmark }
end

これ、なんの意味があるかというと、 HTTP ヘッダや URL の拡張子によって出力フォーマットを変更できるようにしているのです。上記の指定だと、普通にブラウザからアクセスした場合は(通常、ブラウザは最優先の MIME タイプとして "text/html" を指定するために) "format.html" の指定が適用され、 index.html.erb のレンダリング結果が返ります。しかし、 HTTP ヘッダで "application/xml" が優先されていたり、もしくは拡張子が ".xml" だったりすると、 "format.xml" の指定により @bookmark.to_xml がレスポンス・ボディーになります。

例えば、先ほど作ったブックマークアプリケーションで "/bookmarks/1.xml" にアクセスすると、以下のような XML が表示されるはずです。

<?xml version="1.0" encoding="UTF-8"?>
<bookmark>
  <comment>Google Search</comment>
  <created-at type="datetime">2009-02-08T19:24:06Z</created-at>
  <id type="integer">1</id>
  <title>Google</title>
  <updated-at type="datetime">2009-02-08T19:24:06Z</updated-at>
  <url>http://www.google.com/</url>
</bookmark>

拡張子が ".xml" であるために、同じアクションでも出力フォーマットが変化したわけです。

また、同様の仕組みが入力側にもあります。普通に HTML フォームでデータを送信すると "Content-Type" ヘッダは "application/x-www-form-urlencoded" になるので、 Rails は普通にそれを解析して params に格納します。しかし、もし "Content-Type" が "application/xml" であれば、 POST データが XML であると仮定して、それを XmlSimple ライブラリで Hash に変換し、それを params に格納します。

例えば、先ほどの XML と同じものが POST された場合、 params は以下のようになります。

{
  :bookmark => {
    :comment    => "Google Search",
    :created_at => "Sun, 08 Feb 2009 19:24:06 +0000",
    :id         => 1,
    :title      => "Google",
    :updated_at => "Sun, 08 Feb 2009 19:24:06 +0000",
    :url        => "http://www.google.com/"
  }
}

これはまさに、 Scaffold で生成されたフォームによる送信データと同じです。

Rails では、このようにリクエストヘッダの情報等を駆使することで、最小限のコーディングで Web API の実装が可能になっています。また、ここでもやはり入出力される XML のフォーマットに(モデルの to_xml そのままという)規則が生まれるので、 ActiveResource が決め打ちしやすくなっています。

ActiveResource の仕組み

さてさて、ようやくここまで来ましたね(笑)。上記のとおり、 Rails で標準の方法に従ってアプリケーションを作成すれば、自然と XML ベースの Web API が実装できます。 ActiveResource は、 ActiveRecord ライクな CRUD 系のメソッド (find, save, destroy) の呼び出しをそれらの API へのリクエストに変換することで、外部 (Rails) アプリケーションが提供するリソースへの簡便なアクセスを実現します。

具体的な各メソッドとリクエストの対応は以下のようになります。パス中の :collection は実際には "モデルクラス名.tableize" に置き換えられます。

メソッド HTTP メソッド パス
find(:all)
find(:first)
find(:last)
GET /collection
find(id) GET /:collection/:id
save(新規)POST /:collection
save(更新)PUT /:collection/:id
destroy DELETE /:collection/:id

これらのメソッドが呼ばれると、 ActiveResource が上記の URL へリクエストを行い、そのレスポンスを解析してインスタンスに格納します。最初に作ったブックマークアプリケーションを例にして、動作を追っていきましょう。

まず、コンシューマで以下のコードが実行されたとします。

@bookmark = Bookmark.find(1)

すると、 ActiveResource が以下の URL に GET リクエストを投げます。

http://localhost:3000/bookmarks/1.xml

プロバイダはこのリクエストを受け取ると、「レスポンス・フォーマットの切り替え」の仕組みにより、以下の XML を返します。

<?xml version="1.0" encoding="UTF-8"?>
<bookmark>
  <comment>Google Search</comment>
  <created-at type="datetime">2009-02-08T19:24:06Z</created-at>
  <id type="integer">1</id>
  <title>Google</title>
  <updated-at type="datetime">2009-02-08T19:24:06Z</updated-at>
  <url>http://www.google.com/</url>
</bookmark>

コンシューマの ActiveResource は、このレスポンスを解析して各属性値を抽出します。そして、それをもとに新しい Bookmark インスタンスを作成して find メソッドの返り値とします。実際にはいろいろな処理をしていますが、ばっさりと単純化すると以下のような感じです。

Bookmark.new(Hash.from_xml(response.body)['bookmark'])

ActiveResource オブジェクトは ActiveRecord 同様のアクセスメソッドを提供しているので、各属性には以下のようにアクセスできます。

@bookmark.id      # => 1
@bookmark.title   # => "Google"
@bookmark.url     # => "http://www.google.com/"
@bookmark.comment # => "Google Search"
 
# attributes も使えます
@bookmark.attributes['title'] # => "Google"

また、 save, destroy のメソッドも find とほぼ同じように実行されます。

@bookmark = Bookmark.new(:title => "Wikipedia",
                         :url   => "http://www.wikipedia.com/");
 
# POST /bookmarks
@bookmark.save
 
# PUT /bookmarks/#{@bookmark.id}
@bookmark.comment = "Online dictionary"
@bookmark.save
 
# DELETE /bookmarks/#{@bookmark.id}
@bookmark.destroy

アプリケーションからは ActiveRecord オブジェクトとほぼ同様に扱えるため、さきほどのブックマークアプリケーションのように Scaffold で生成されたコントローラ・ビューで表示・更新などができるわけです(モデルに若干の修正が必要なのは見逃してください ^^;)。このように、 Rails の MVC アーキテクチャに調和する形で Web API の機能をアプリケーションに取り込める点が、 ActiveResource の最大の利点です。

その他の機能

以上が ActiveResource の基本ですが、その他にも便利な機能がいろいろあります。ここでは、よく使うであろう機能を簡単にご紹介します。

カスタムメソッド

例えば、先ほどのサンプルで、各ブックマークにスターを付ける機能を実装したとしましょう。プロバイダのルーティングを以下のように変更されます。

map.resources(:bookmark,
              :collection => { :stars => :get }
              :member     => { :star  => :put })

これで適切にアクションを実装すれば、 "/bookmarks/stars.xml" でスターの付いたブックマークのリスト取得が、 "/bookmarks/:id/star.xml" でブックマークにスターを付ける機能が実現できます。

さて、 ActiveResource でこれらの追加されたアクションにアクセスするには、どうすればいいでしょうか。それには「カスタムメソッド」という機能を利用します。まず、 "/bookmarks/stars" からスター付きブックマークを取得するには、 find メソッドの :from パラメータを利用します。

@stars = Bookmark.find(:all, :from => :stars)

これで "/bookmarks/stars" に GET リクエストを送り、そのレスポンスを通常の find と同様に解析して、 Bookmark インスタンスの配列を返します。

次に、 "/bookmarks/:id" に PUT リクエストを送るには、 put メソッドを使います。

Bookmark.find(1).put(:star, { :star => true }, '')

こうすると、 "/bookmarks/1?star=1" に PUT リクエストを送信します。もちろん put のほかに get, post, delete もありますので、追加されたアクションに対して自由にリクエストを送信できます。

カスタムメソッドの使い方は、次回でより詳細にご紹介する予定です。

Validation を設定する

ValidationRails (ActiveRecord) の素晴らしい機能のひとつですが、 ActiveResource ではプロバイダで発生した Validation エラーの情報がそのままコンシューマでも利用できます。例えばブックマークアプリケーションでプロバイダの ActiveRecord モデルに以下のような Validation を設定したとします。

class Bookmark < ActiveRecord::Base
  validates_presence_of :title
end

そして、コンシューマでタイトルが空のブックマークを作成しようとすると、

このようにエラーが表示されます。

この機能は(プロバイダの) Scaffold が生成した create, update アクションの、以下のコードで実現されています。

respond_to do |format|
  if @bookmark.save
    flash[:notice] = 'Bookmark was successfully created.'
    format.html { redirect_to(@bookmark) }
    format.xml  { render :xml => @bookmark, :status => :created, :location => @bookmark }
  else
    format.html { render :action => "new" }
    format.xml  { render :xml => @bookmark.errors, :status => :unprocessable_entity }
  end
end

つまり、 Validation エラーが発生すると、モデルの内容の代わりにエラー情報を格納した XML が返信されるのです。すると、それをコンシューマの ActiveResource が認識し、自動で @bookmark.errors にエラー情報を復元してくれます。あとは ActiveRecord と同じ方法でそれをハンドリングすれば、めでたくエラーが表示されるというわけです。

ちなみに、リファレンスにはクライアントサイドでの Validation も可能という記述がありますが、コードを読む限り、こちらはまだ機能していないようです。

BASIC 認証を行う

多くの場合、 API を無制限に公開するのではなく、なんらかの認証を設定したいと思うことでしょう。そのため、 ActiveResource は標準で BASIC 認証をサポートしています。今どき BASIC 認証?という気がしなくもないですが、とても簡単に使えるので、それなりに便利だと思います。

それでは、ここでもやはりブックマークアプリケーションを例にして、 BASIC 認証を使ってみましょう。まずはプロバイダに BASIC 認証をかけるため、 app/controllers/bookmarks_controller.rb に以下のコードを追加します。

class BookmarksController < ApplicationController
  before_filter :authenticate
  # ...
 
  private
  def authenticate
    authenticate_or_request_with_http_basic do |user_name, password|
      user_name == 'user' && password == 'password'
    end
  end
end

そして、コンシューマの app/models/bookmark.rb で、ユーザー名とパスワードを指定します。

class Bookmark < ActiveResource::Base
  self.site = 'http://localhost:3000/'
  self.user = 'user'
  self.password = 'password'
  #...
end

なんと、これだけで作業終了です。お手軽ですね。プロバイダに直接アクセスするとパスワードを尋ねられますが、コンシューマを介せばパスワードなしで参照・更新できるはずです。

なお、多少手間はかかりますが、独自の認証システムに対応することも可能です。その方法は第 3 回でご紹介する予定ですので、お楽しみに。

ユニットテスト

Web API を利用した開発で、頭の痛い問題のひとつがテストです。サードパーティーの API からは簡単に BAN されてしまいますし、内部のシステムだったとしても、フィクスチャのセットアップなどはとても面倒な作業になります。

そんな問題も、 ActiveResource ならバッチリ解決してくれます。 HTTP アクセスのモックアップが標準でサポートされているのです。例として、ブックマークアプリケーションで簡単なユニットテストを作ってみました。以下がそのソースです。

require 'test_helper'
require 'active_resource/http_mock'
 
class BookmarkTest < ActiveSupport::TestCase
  def setup
    @record = {
      :id      => 1,
      :title   => 'WebOS Goodies',
      :url     => 'http://webos-goodies.jp/',
      :comment => 'Welcome!'
    }.to_xml(:root => 'bookmarks')
    @header = Bookmark.connection.__send__(:build_request_headers, {}, :get)
    ActiveResource::HttpMock.respond_to do |mock|
      mock.get '/bookmarks/1.xml', @header, @record
    end
  end
 
  test "get bookmark" do
    bookmark = Bookmark.find(1)
    assert_equal 'WebOS Goodies',            bookmark.title
    assert_equal 'http://webos-goodies.jp/', bookmark.url
    assert_equal 'Welcome!',                 bookmark.comment
    expected_request = ActiveResource::Request.new(:get, '/bookmarks/1.xml', nil, @header)
    assert ActiveResource::HttpMock.requests.include?(expected_request)
  end
end

まず、 2 行目で require している 'active_resource/http_mock' がモックアップ用のライブラリです。標準では読み込まれないので、明示的に require しなければなりません。

そして、モックを定義しているのが以下の部分です。

ActiveResource::HttpMock.respond_to do |mock|
  mock.get '/bookmarks/1.xml', @header, @record
end

"/bookmarks/1.xml" に対して、リクエストヘッダ @header でリクエストが行われたときは、(実際にはリクエストを送信せずに) @record をレスポンス・ボディーとして返す」ということを定義しています。上記ではひと組のリクエスト・レスポンスしか定義していませんが、もちろん複数定義することも可能ですし、 get 以外に post, put, delete も使えます。詳細はリファレンスをご参照ください。

あとは普通に ActiveResource のメソッドを呼び出してテストを実行し、そして最後に以下のコードで本当にリクエストが発行されたかどうかを確認します。

expected_request = ActiveResource::Request.new(:get, '/bookmarks/1.xml', nil, @header)
assert ActiveResource::HttpMock.requests.include?(expected_request)

ActiveResource::HttpMock.requests で発行されたすべてのリクエストが取得できますので、それを調べることで必要なリクエストが正しく発行されたかどうかをチェックできるというわけです。

このように、 HttpMock を使えば実際にはリクエストを発行せずに、 ActiveResource のテストが行えます。今回はわかりやすさを優先してユニットテストを題材にしましたが、もちろん機能テストなどでも同様に利用できます。むしろ、そちらで使う方が意味のあるテストになるでしょう。ぜひ活用して、開発効率の向上に役立ててください。

Rails の外で利用する

前述のとおり、 ActiveResourceRails を使わない独立したスクリプトでも利用できます。例えば、以下はブックマークアプリケーションに登録されたブックマークの情報を表示するコマンドラインツールです。

#! /usr/bin/ruby
 
require 'rubygems'
require 'active_support'
require 'active_resource'
 
class Bookmark < ActiveResource::Base
  self.site = 'http://localhost:3000/'
  self.user = 'user'
  self.password = 'password'
end
 
Bookmark.find(:all).each do|bookmark|
  print <<EOS
title: #{bookmark.title}
url:   #{bookmark.url}
comment:
#{bookmark.comment}
 
EOS
end

このように、 ActiveResourceRails アプリケーションを外部のスクリプトから操作する目的でも使うことができます。データのエクスポートやバッチ処理など、いろいろと応用が考えられますね。もちろん、 Rails 以外の Web API でも、モデルさえ用意すれば扱うことが可能です。

以上、本日は ActiveResource で 2 つの Rails アプリケーション間の通信を実現する方法をご紹介しました。極めて簡単に 2 つの Rails アプリケーションを連携させられることがおわかりいただけたと思います。ただ、今回は極めて単純なケースに適用しただけなので、果たして実際のアプリケーションに応用できるかどうか疑問を持たれているかもしれませんね。次回はそのあたりに対応するためのコンフィギュレーションを中心に、さらに詳細な使い方をご紹介していきます。どうぞお楽しみに!

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles