Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Rails2.0の足回りと中級者への道

第2回Rails2.0で作るRESTfulアプリケーション(前編)

前回の記事では、Rails2.0の足回りを簡単に概観しました。今回は、実際にRails2.0の機能を利用し、RESTfulなウェブアプリケーションを作ってみたいと思います。

RESTとは何か

Rails2.0の機能を用いて、RESTfulなアプリケーションを作るまえに―

RESTとは、いったいなんでしょうか?

という問いに対して、正確に答えるには私の知識はこころもとないです。Wikipedia日本語版のRESTの項を引いてみると、⁠表現可能な状態を転送するもの」と書かれてありますが、これだけ翻訳してもよくわかりませんね。用語としての初出は、2000年に、HTTPプロトコル規格の主要著者の一人であるRoy Fieldingがウェブについて書いた博士論文Architectural Styles and the Design of Network-based Software Architecturesです。RESTとはそこでFieldingが提案した、Webのアーキテクチャにおけるいくつかの原則です。

なお、Roy FieldingはHTTPプロトコルの主要著者と言うだけでなく、IETFにおいてHTML、URIの標準化に参与した人物であり、Apacheプロジェクトの創始者で、現在もApache HTTP Serverプロジェクトを率いています。彼のことを一言で表現するとまさに、⁠World Wide Webの神」です。

それでは、Fieldingの論文における「RESTの原則」とはどういったものでしょうか? もっとも重要なのは以下の2点ではないかと思われます。

ステートレスなクライアントサーバ方式

RESTの原則では、HTTPのリクエストに全ての情報を含み、サーバ側に、クライアントの状態を持ちません。この原則は、情報の可視性、アプリケーションの信頼性、スケーラビリティにおいてメリットを持ちます。信頼性、スケーラビリティのもっとも単純な例として、静的なHTMLファイルを想起するとよいかもしれません。デメリットとしては、リクエストに全ての情報を持ってしまうことでデータ送信量が増加し、ネットワークのパフォーマンスの低下の発生が考えられます。

統一インターフェース(Uniform Interface)

RESTを他のアーキテクチャと区別する中心となる概念が、統一インターフェースです。⁠統一インターフェース」とは、コンポーネント間のやりとりの全てを、リソースを表すURIと、それにたいするHTTPのメソッドで行いましょう、ということです。

インターフェースを統一化することで、アーキテクチャはシンプルになり、コンポーネント間の作用の可視性が増します。これに対する、単純、かつ最強の例としては、HTMLとURIによるリンクによって形づくられた、World Wide Web自体を想起するとよいでしょう。デメリットはフォーマットを統一化するオーバーヘッドによる実行効率の低下です。

RESTfulなURI

RESTの統一インターフェースに対する、わかりやすいたとえとして以下のものがあります。

RESTを文と考える。すると、HTTP Methodsは動詞で、URIは名詞である。

具体的な例を挙げてみましょう。

POST http://bookmark/links
⇒投稿する(動詞)ブックマーク(名詞)リンク(名詞)
GET http://bookmark/links/1
⇒取得する(動詞)ブックマーク(名詞)リンク(名詞)1(名詞)
GET http://bookmark/links/1.xml
⇒取得する(動詞)ブックマーク(名詞)リンク(名詞)1(名詞)XML(名詞)
DELETE http://bookmark/links/1
⇒削除する(動詞)ブックマーク(名詞)リンク(名詞)1(名詞)

この観点からすると、以下のようなURIに矛盾があることがわかります。

GET http://bookmark/links/destroy/1
⇒取得する(動詞)ブックマーク(名詞)リンク(名詞)削除(動詞)1(名詞)

統一インターフェースとしてのRESTfulなURIとは、URIを名詞と考え、HTTPのメソッドを動詞とした際に自然な形になるものといえます。

RESTのイメージだけでもつかんでいただけましたでしょうか?

Rails2.0のRESTサポートの優れた点の一つに、それ自体がRESTの問いに対する明快な回答である、という点があります。そのため、RailsのRESTサポートを見ていくことで、RESTの概念を学習することができるという一石二鳥なメリットもあります。

Railsで作る小さなRESTful Webアプリケーション

さてこのあたりで、今回の題材として、ソーシャルブックマークの代名詞となった、del.icio.us風のブックマークアプリケーションをRESTスタイルで作ってみましょう。安易ですが小さなdeliciousということで、その名もminiciousです。

それでは早速、railsコマンドでRailsアプリケーションのフォルダを作成します。

rails minicious

miniciousのDBスキーマ設計

ソーシャルブックマークにはどんなデータが必要でしょうか?

del.icio.usの画面を参考にすると、最低限、⁠ユーザ」⁠ブックマーク対象のURL」⁠タグ」といった情報が必要でありそうなことがわかります。それでは、それらのデータを保持する最初の叩き台のモデルを作成してみましょう。

Railsでのスキーマ定義を行うMigrationファイルを作成します。

./script/generate migration CreateTables

上記のコマンドの実行で、⁠db/migrate/001_create_tables.rb」というテンプレートファイルが作成されました。これを修正してDBスキーマ設計を行います。

今回のMigrationでは、Rails 2.0から採用になったSexyMigrationの機能を利用してみましょう。Rails2.0以前のMigrationとどこが異なるのかについて見ていきます。

Rails2.0以前(非SexyMigration)
t.column :カラム名, :型名
t.column  :カラム名, :型名
.
.
.

Rails2.0以前ではカラムの数だけ、Migrationに設定行が必要でした。Rails2.0のSexyMigrationではこうなります。

Rails2.0 ⁠SexyMigration)
t.型名 :カラム名, :カラム名,.... (可変長の指定)

:型名ではなく、t.型名 :カラム名...の可変長の形式の指定が可能になりました。これによって設定の行数を削減することができます。

SexyMigrationはそれだけではありません。

:has_many, :belongs_toといった関連を作成している場合の、外部キーの指定も簡単になります。

Rails2.0以前(非SexyMigration)
t.column :外部キーID, :integer
t.column :外部キーID, :integer
.
.
Rails2.0 ⁠SexyMigration)
t.references :関連元テーブル名(単数形), 関連元テーブル名(単数形),...

t.referencesの後に、関連元のテーブル名を単数系で指定することができます。

それではこれらの機能をもちいて、実際にMigrationファイルにDBスキーマ設計を記述していきます。

db/migrate/001_create_tables.rb
   class CreateTables < ActiveRecord::Migration
   def self.up
     create_table :users do |t|
       t.text :login, :password
       t.timestamps
     end
     create_table :links do |t|
       t.references :user
       t.text :url, :notes
       t.timestamps
     end
     create_table :tags do |t|
       t.text :name
       t.timestamps
     end
     create_table :assorts do |t|
       t.references :link, :tag
       t.timestamps
     end
  end


   def self.down
     drop_table :users
     drop_table :links
     drop_table :tags
     drop_table :assorts
  end
end

最初のDBスキーマ設計ができました。rakeコマンドを用いて、DBにテーブルを作成します。

実テーブル作成
rake db:migrate

Railsでのスキーマ設計はここで終了ではなく、開発を進めていく上で矛盾が発生すれば、その都度あらたなMigrationファイルを作成し、データベースリファクタリングをすすめていくスタイルになります。

Modelの作成と関連の指定

同様にモデルの作成と、関連の指定も行います。こちらについては、Rails2.0以前と同様ですのでコード例だけにとどめます。

さきにMigrationファイルを一括して作成したため、それぞれのModel作成では--skip-migrationオプションを指定し、Migrationテンプレートの作成をスキップしています。

Modelのテンプレート作成
./script/generate model user --skip-migration
./script/generate model link --skip-migration
./script/generate model tag --skip-migration
./script/generate model assort --skip-migration
app/models/user..rb
class User < ActiveRecord::Base
  has_many :links
end
app/models/link.rb
class Link < ActiveRecord::Base
  has_many :assorts
  has_many :tags, :through=>:assorts
  belongs_to :user
end
app/models/tag.rb
class Tag < ActiveRecord::Base
  has_many :assorts
  has_many :links, :through=>:assorts
end
app/models/assort.rb
class Assort < ActiveRecord::Base
  belongs_to :link
  belongs_to :tag
end

RESTful scaffold

前回の記事でも利用しましたが、Rails2.0以降で標準となった、RESTfulなscaffoldを利用して足がかりを作成しておきましょう。ここでも、--skip-migrationオプションを指定しています。

scaffold コマンド
./script/plugin install scaffolding
./script/generate scaffold link url:string  notes:text --skip-migration

RESTfulなログイン

実は、ここまでの段階で、RESTfullなブックマークのCRUDを行うアプリケーションは出来てしまっています。RESTであるかどうかなどはほとんど気にせず、すんなりRailsが私たちをRESTの世界に運んでくれます。

手が止まるのは、ソーシャルブックマークの次の一歩。ログインです。

RESTの概念と、ログインすなわち認証の枠組みは相性が悪いことで知られています。⁠サーバ内で認証済み」という状態そのものであるログインと、サーバ側では状態を持たず、全てのリクエストに情報を持つRESTのそりが合わないのはある意味当然です。この問いに対してRailsではどう回答するのでしょうか?

今回述べる一つの解は、以下のものです。

  • WebブラウザからのHTMLフォーム経由のアクセスの際はHTTPセッションを利用する
  • RESTfulなURIに対するXMLアクセスの際はHTTPヘッダによるベーシック認証を利用する

サーバ側で状態を持たないはずのRESTでセッションを使ってしまっていますが、Web APIとしてだけの提供だけではなく、Webアプリケーションとしても動作させる場合、現状これが現実解ではないかと思われます。

セッションによるログイン

具体例を見てみましょう。

HTML経由のアクセスの場合はHTTPセッションを使います。ログイン用のモデルを持たない、SessionControllerを作成し、ログインした際にcreateアクションでセッションの作成、ログアウトの際にはセッションを削除するdestroyアクションを追加します。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    if self.current_user = User.authenticate(params[:user][:login], params[:user][:password])
      flash[:notice] = "Wellcome #{current_user.login}"[:wellcome_message]
      redirect_to home_url
    else
      flash[:notice] = "Invalid login or password, try again."[:invalid_login_message]
      redirect_to login_url
    end
  end

  def destroy
    session.delete
    cookies.delete :login_token
    flash[:notice] = "You have been logged out."[:logged_out_message]
    redirect_to login_url
  end
end

SessionControllerを使うために必要なRouting設定は以下のようになります。前回も少し触れましたが、map.resources、map.resourceがRails2.0でRESTfulなCRUDを行うURIを作成する魔法です。次回、ActiveResourceの解説の際に詳しく説明します。

config/route.rb
ActionController::Routing::Routes.draw do |map|
  map.home '', :controller => 'links', :action => 'index'
  map.resources :links
  map.resource :session
  map.login    'login',         :controller => 'sessions', :action => 'new'
  map.logout  'logout',      :controller => 'sessions', :action => 'destroy'
end

rakeのroutesコマンドで、上記のRoutingで作成したURIを可視化してみます。map.resouces,map.resourceが自動でRESTfulなURIを作成していることがわかります。

通常、統一インターフェースを最重要とするRESTfulなサービス開発では、リソース設計、URI設計にもっとも重きを置かれています。ですがRailsでは、map.resources,map.resourceを使うことで、なにも考えずにRailsの規約に従ったRESTfulなURIが作成されます。

rake routes
% rake routes
(in /home/user/rails/minicious)
                  home        /                       {:controller=>"links", :action=>"index"}
                 links GET    /links                  {:controller=>"links", :action=>"index"}
       formatted_links GET    /links.:format          {:controller=>"links", :action=>"index"}
                       POST   /links                  {:controller=>"links", :action=>"create"}
                       POST   /links.:format          {:controller=>"links", :action=>"create"}
              new_link GET    /links/new              {:controller=>"links", :action=>"new"}
    formatted_new_link GET    /links/new.:format      {:controller=>"links", :action=>"new"}
             edit_link GET    /links/:id/edit         {:controller=>"links", :action=>"edit"}
   formatted_edit_link GET    /links/:id/edit.:format {:controller=>"links", :action=>"edit"}
                  link GET    /links/:id              {:controller=>"links", :action=>"show"}
        formatted_link GET    /links/:id.:format      {:controller=>"links", :action=>"show"}
                       PUT    /links/:id              {:controller=>"links", :action=>"update"}
                       PUT    /links/:id.:format      {:controller=>"links", :action=>"update"}
                       DELETE /links/:id              {:controller=>"links", :action=>"destroy"}
                       DELETE /links/:id.:format      {:controller=>"links", :action=>"destroy"}
                       POST   /session                {:controller=>"sessions", :action=>"create"}
                       POST   /session.:format        {:controller=>"sessions", :action=>"create"}
           new_session GET    /session/new            {:controller=>"sessions", :action=>"new"}
 formatted_new_session GET    /session/new.:format    {:controller=>"sessions", :action=>"new"}
          edit_session GET    /session/edit           {:controller=>"sessions", :action=>"edit"}
formatted_edit_session GET    /session/edit.:format   {:controller=>"sessions", :action=>"edit"}
               session GET    /session                {:controller=>"sessions", :action=>"show"}
     formatted_session GET    /session.:format        {:controller=>"sessions", :action=>"show"}
                       PUT    /session                {:controller=>"sessions", :action=>"update"}
                       PUT    /session.:format        {:controller=>"sessions", :action=>"update"}
                       DELETE /session                {:controller=>"sessions", :action=>"destroy"}
                       DELETE /session.:format        {:controller=>"sessions", :action=>"destroy"}
                 login        /login                  {:controller=>"sessions", :action=>"new"}
                logout        /logout                 {:controller=>"sessions", :action=>"destroy"}

Basic認証によるログイン

次は、RESTfulなURIに対するBasic認証です。

ここで特に重要なのはlogin_requiredメソッドです。RESTfulなURIへのXMLでのアクセスの場合、HTTPのヘッダを利用してRESTfulなリクエストの認証を行います。認証できなかった場合、再度ログインを促します。

/app/controllers/application.rb
class ApplicationController < ActionController::Base
  helper :all
  helper_method :current_user_id, :logged_in?

  protect_from_forgery

  protected


  # XML でのアクセスの場合、HTTP のヘッダを利用して RESTful なリクエストの認証を行います
  # 認証できなかった場合、エラーを返します
  def login_required
    respond_to do |format|
      format.html { redirect_to login_path }
      format.xml do
        if user = authenticate_with_http_basic { |u, p| User.authenticate(u, p) }
          @current_user = user
        else
          request_http_basic_authentication
        end
      end
    end unless logged_in?
  end                           

  def current_user_id
     if logged_in?
       @current_user.login
     else
       "guest"       
     end
  end

  def current_user=(value)
    if @current_user = value
      session[:user_id] = @current_user.id
    end
  end

  def current_user
    @current_user ||= ((session[:user_id] && User.find_by_id(session[:user_id])) || 0)
  end
   
  def logged_in?
    current_user != 0
  end
   
end

それではlogin_requiredメソッドを解説します。

まず、respond_toが、RailsでURIのリソースの表現の違いを処理する、RESTfulなWebアプリケーションにとって非常に重要なメソッドです。上の例ではURIに対するHTMLリソースへのアクセスは、login_path (/login)に飛ばしています。

また、HTTP Basic認証の補助処理を行うヘルパーメソッドauthenticate_with_http_basic, request_http_basic_authenticationを使用しています。これらのメソッドは、Rails バージョン 2.0.2の場合、actionpack-2.0.2/lib/action_controller/http_authentication.rbにてHttpAuthenticationというmoduleとして定義されています。上の例では、authenticate_with_http_basicメソッドで認証を受け取り、request_http_basic_authenticationメソッドで認証を要求します。

認証を利用する側の、LinkControllerでは以下の修正を行います。例では、全てのアクションに対して認証のフィルターをかけています。

/app/controllers/links_controller.rb から抜粋
class LinksController < ApplicationController                        
                                                                                        
  before_filter :login_required
.
.
end

RESTfulなURIに対するXMLリソースへのアクセス

ここまでの段階で、RESTfulなURIにたいして、認証込みのアクセスが可能かどうかためしてみましょう。

なにはともあれ、script/severでRailsサーバを立ち上げます。

% script/server

Basic認証は、HTTPのAuthorizationヘッダーに、user:passwordをBASE64でエンコードした文字列を付与することで行います。

loginのIDがuser、passwordがuserの場合、以下のようなRubyワンライナーでAuthorizationヘッダーに設定する文字列を作成できます。

% ruby -e "require 'base64';puts Base64.encode64('user:user')"
% dXNlcjp1c2Vy

ここでは、GNU wgetコマンドをもちいてアクセスしてみます(既にデータは作成済みと仮定します⁠⁠。

XMLリソースへの認証つきアクセス(整形しています)
% wget http://localhost:3000/links.xml -d --header="Authorization: Basic dXNlcjp1c2Vy" -O -

(略)
---request begin---
GET /links.xml HTTP/1.0
User-Agent: Wget/1.10.2
Accept: */*
Host: localhost:3000
Connection: Keep-Alive
Authorization: Basic dXNlcjp1c2Vy
(略)
---response begin---
HTTP/1.1 200 OK
Connection: close
Date: Thu, 01 May 2008 19:44:49 GMT
Set-Cookie: _minicious_session=BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNo%250ASGFzaHsABjoKQHVzZWR7AA%253D%253D--8b433eee9710a010fb4879e70a25b36bfd7d7f23; path=/
Status: 200 OK
X-Runtime: 0.05679
ETag: "00bb9b2de47ff02123610da2ec9592f8"
Cache-Control: private, max-age=0, must-revalidate
Server: Mongrel 1.1.4
Content-Type: application/xml; charset=utf-8
Content-Length: 351

(略)
<?xml version="1.0" encoding="UTF-8"?>
<links type="array">
  <link>
    <created-at type="datetime">2008-04-29T19:31:03+09:00</created-at>
    <id type="integer">1</id>
    <notes></notes>
    <updated-at type="datetime">2008-04-29T19:31:03+09:00</updated-at>
    <url>http://gihyo.jp</url>
  </link>
</links>

なお、Authenticateヘッダーではなく以下のURIでも同様にBasic認証が行えます。テスト中に簡易に確認したい場合は便利です。

% wget http://user:user@localhost:3000/links.xml -O -

ERbファイルの修正

最後に、scaffold で自動作成された ERbファイルを修正してデザインを整えていきます。具体的な修正点は以下のファイルです。注意点として、layoutファイルを、application.html.erbとして一つにまとめています。

app/views/layouts/application.html.erb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>minicious: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<h2 align="left">
<%= link_to 'minicious', home_path %>
</h2>
<table width="100%">
<tr>
<td align="left" width="50%"><%= link_to 'post', new_link_path %></td>
<td align="right" width="50%">
logged in as <%= current_user_id %> |
<% unless logged_in?%><%= link_to 'login', login_path %> |<% end %>
<%= link_to 'logout', logout_path %>
</td>
</table>
<hr>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield  %>

</body>
</html>
app/views/links/index.html.erb
<h1>links</h1>
<table>
<% for link in @links %>
  <tr>
    <td><%= link_to link.url, link.url %></td>
    <td><%=h link.notes %></td>
    <td><%= link_to 'edit', edit_link_path(link) %> /</td>
    <td><%= link_to 'destroy', link, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

画面イメージとソースコード

以下がログイン画面です。ログイン画面で「ログインのID」⁠パスワード」を入力すると、リンク一覧画面にリダイレクトされます。

ログイン画面
ログイン画面

作成したリンクの一覧は以下のように表示されます。

リンク一覧
リンク一覧

なお、以下が現段階のminiciousソースアーカイブです。ログインID:user, パスワード:user のユーザが作成されています。

  • minicious_rails2_0002_2.zip
    2008年5月20日18時以前のアーカイブには誤りがありました。それ以前に取得された方はお手数ですが再度取得していただくようお願いいたします。

現在ユーザ管理の画面が無いため、別のユーザアカウントを作成したい場合、直接 SQL 文でインサートを行うか、コンソールでUserオブジェクトを作成する必要があります。

以下は、ログインID: gihyo, パスワード:gihyo のユーザ作成例です。

script/console
>> User.create({:login=>"gihyo", :password=>"gihyo"})
=> #<User id: 2, login: "gihyo", password: "gihyo", created_at: "2008-05-15 12:21:34", updated_at: "2008-05-15 12:21:34">

まとめと次回予告

Rails2.0のRESTサポートの紹介とともに、最低限の機能を持つ、RESTfulなソーシャルブックマークを作ってみました。

次回は、今回作成したアプリケーションをRails2.0の機能を用いてさらに改良していきたいと思います。

おすすめ記事