こんにちは。まっつんです。今回は Ruby on Rails (以下、Rails) を使った RESTful な Web アプリケーションの実装についてに見ていきたいと思います。

まず REST について考えるところからはじめましょう。

MATSUFUJI Hideharu

REST を構成する要素

第 1 回 で、RESTWWW のアーキテクチャスタイルであるということがわかりました。RESTWWW の関係について、山本陽平 氏はブログで以下のように述べられています。

yohei-y:REST 入門(その2) アーキテクチャスタイルとは?
アーキテクチャスタイルとは特定の実装を指すものではないことに注意してください。 WWW は REST の一実装形態です。WWW 以外の REST の実装も考えられます。しかし、現実には REST といえば WWW のアーキテクチャスタイルを指す場合が多いので、これからは特にことわりなく REST の実装例として WWW を使っていきます。

今回はこの考えに従って、WWW を前提に話を進めていきます。

REST には大きく三つの要素があります。これらの要素と WWW における実装技術の対応は以下のとおりです。

RESTWWW
リソースHTML, XML など
リソースの識別URI
リソースの操作HTTP メソッド

Rails における REST

Rails では REST の三つの要素「リソース」、「リソースの識別」、「リソースの操作」はどのように実装されるのでしょうか?

リソース - モデルとビュー

一般的な Web アプリケーションでは、なんらかのデータベースにリソースを格納することになります。Rails は、このような構造を持った Web アプリケーションの開発を前提としています。

Rails は、クライアントからリクエストされたリソースをモデルを介してデータベースから取得し、ビューに表示します。ビューには HTML や XML, JSON などさまざまなフォーマットを使用することができます。

以上のことから、Rails では、リソースがモデルとビューによって表現されている、ということができます。

リソースの識別と操作 - URI マッピング

Rails では RESTful な URI マッピングをデフォルトのマッピングルールとして採用しています。この点からも RailsREST アーキテクチャスタイルを Web アプリケーションのあるべき姿として考えていることがわかります。

なお、Rails のドキュメントでは「URI マッピング」が「ルート」と表現されていますが、「URI マッピング」という表現の方が理解しやすいのではないかという判断から、今回は「URI マッピング」として解説します。

URI マッピングは config/routes.rb で定義します。第 2 回 ではページフローを表現するために多少複雑な定義になりましたが、デフォルトのマッピングルールを採用できる場合は以下のような記述のみで事足ります。

map.resources :orders

REST に関連する部分の URI, HTTP メソッド、アクションはそれぞれ以下のようにマッピングされます。

URIHTTP メソッドアクション説明
/ordersGETindexすべての Order を表示する
/ordersPOSTcreateOrder を作成する
/orders/{order_id}GETshowID が {order_id} の Order を表示する
/orders/{order_id}PUTupdateID が {order_id} の Order を更新する
/orders/{order_id}DELETEdestroyID が {order_id} の Order を削除する

次に、Scaffolding によって作成されたコントローラの update メソッドを見てみましょう。

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
...
  def update
    @order = Order.find(params[:id])

    respond_to do |format|
      if @order.update_attributes(params[:order])
        flash[:notice] = 'Order was successfully updated.'
        format.html { redirect_to(@order) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @order.errors, :status => :unprocessable_entity }
      end
    end
  end
...
end

リクエストパラメータから取得した ID で検索し、見つかった Order オブジェクトを更新しています。このようにひとつのリクエストで処理が完了する、即ちステートレスであることが REST の特徴のひとつとして挙げられます。

関連するリソースの表現

多くの場合、リソースはほかのリソースと関連があります。ここではブログの Entry (記事) とそれに対する Comment (コメント) を例に Rails がどのよう関連するリソースを扱うのかを見てみましょう。

URIコントローラアクション説明
/entries/{entry_id}Entryshow記事 ID が {entry_id} の記事とコメントの件数を表示する
/entries/{entry_id}/commentsCommentindex記事 ID が {entry_id} のコメントの一覧を表示する
/entries/{entry_id}/comments/{comment_id}Commentshow記事 ID が {entry_id}, コメント ID が {comment_id} のコメントを表示する
/commentsCommentindexコメントの一覧を表示する

今回の URI 設計の方針は、Comment の作成や削除は /comments ではなく /entries/{comment_id}/comments に対して行い、/comments は一覧の表示のみに使うというものです。

それでは実装してみましょう。まず Scaffolding で Entry と Comment のモデル、ビュー、コントローラを作成し、マイグレーションを行います。

script/generate scaffold Entry title:string content:string
script/generate scaffold Comment comment:string entry_id:integer
rake db:migrate

次に、モデルに Entry と Comment の関連を追加します。Entry を基準に考えた場合、Comment は 1 対 多 の関係になるので has_many となります。Comment はひとつの Entry に従属するので belongs_to として定義します。

app/models/entry.rb
diff --git a/app/models/entry.rb b/app/models/entry.rb
index e464bb4..e2f07a8 100644
--- a/app/models/entry.rb
+++ b/app/models/entry.rb
@@ -1,2 +1,3 @@
 class Entry < ActiveRecord::Base
+  has_many :comments
 end
app/models/comment.rb
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 45b2d38..2727989 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -1,2 +1,3 @@
 class Comment < ActiveRecord::Base
+  belongs_to :entry
 end

次に、Entry が複数の Comment を持つように URI マッピングの定義を追加します。

app/models/comment.rb
diff --git a/config/routes.rb b/config/routes.rb
index 51f3714..e360102 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,8 @@
 ActionController::Routing::Routes.draw do |map|
   map.resources :comments
 
-  map.resources :entries
+  map.resources :entries,
+    :has_many => :comments
 
   map.resources :customers

/entries/{entry_id} でコメントの件数を表示し、/entries/{entry_id}/comments にリンクするようにビューを変更します。

app/views/entries/show.html.erb
diff --git a/app/views/entries/show.html.erb b/app/views/entries/show.html.erb
index 798a63c..08ab0d9 100644
--- a/app/views/entries/show.html.erb
+++ b/app/views/entries/show.html.erb
@@ -8,5 +8,10 @@
   <%=h @entry.content %>
 </p>
 
+<p>
+  <b>コメント </b>
+  <%= link_to "#{@entry.comments.size} 件", entry_comments_url(@entry) %>
+</p>
+
 <%= link_to 'Edit', edit_entry_path(@entry) %> |
 <%= link_to 'Back', entries_path %>

/entries/{entry_id}/comments/{comment_id} で記事のタイトルを表示し、/entries/{entry_id} にリンクするようにビューを変更します。

app/views/comments/show.html.erb
diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb
index 1928a1a..97264b2 100644
--- a/app/views/comments/show.html.erb
+++ b/app/views/comments/show.html.erb
@@ -1,13 +1,11 @@
 <p>
-  <b>Comment:</b>
-  <%=h @comment.comment %>
+  記事 <%= link_to "#{@comment.entry.title}", entry_url(@comment.entry) %> へのコメント
 </p>
 
 <p>
-  <b>Entry:</b>
-  <%=h @comment.entry_id %>
+  <b>Comment:</b>
+  <%=h @comment.comment %>
 </p>
 
-
 <%= link_to 'Edit', edit_comment_path(@comment) %> |
-<%= link_to 'Back', comments_path %>
\ No newline at end of file
+<%= link_to 'Back', comments_path %>

最後に Comment のコントローラの index メソッドを変更します。

app/controllers/comments_controller.rb
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 92acb07..e90d608 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -2,7 +2,11 @@ class CommentsController < ApplicationController
   # GET /comments
   # GET /comments.xml
   def index
-    @comments = Comment.all
+    if params[:entry_id]
+      @comments = Comment.all(:conditions => ["entry_id = ?", params[:entry_id]])
+    else
+      @comments = Comment.all
+    end
 
     respond_to do |format|
       format.html # index.html.erb

ここで重要なのは /entries/{entry_id}/comments と /comments の両方からのアクセスを考慮する必要があるという点です。今回はリクエストパラメータの有無によって、検索条件を指定するかどうかを判断しています。

以上で実装は完了です。/entries/new から Entry を二件追加し、Comment に登録します。今回は話を単純にするため SQL で直接 Comment を登録することにします。

INSERT INTO comments(comment, entry_id) VALUES('人生が変わりました', 1);
INSERT INTO comments(comment, entry_id) VALUES('まっつんは命の恩人です', 1);
INSERT INTO comments(comment, entry_id) VALUES('まっつんは神', 1);
INSERT INTO comments(comment, entry_id) VALUES('このコメントは二つ目の記事のコメントです', 2);

最初の 3 件は 記事 ID が 1 の Entry の Comment、最後の 1 件は 記事 ID が 2 の Entry の Comment です。では /entries/1 にアクセスして、Comment の件数 3 になることを確認しましょう。

rails3-entry-show.png

次に、/entries/1/comments にアクセスして、記事 ID が 1 の Comment の一覧が表示されていることを確認します。

rails3-entry-comment-index.png

さらに、/entries/1/comments/1 にアクセスして、Comment の内容が表示されていることを確認します。

rails3-entry-comment-show.png

最後に、/comments/ にアクセスして、Comment の一覧が表示されていることを確認します。

rails3-comment-index.png

このように Rails では、モデルの関連と URI マッピングの関連をそれぞれ別に定義することで関連するリソースを表現することができます。

今回の実装では、以下の機能が実現できませんでした。

  • /comments/{entry_id} で各 Comment を操作できないようにする
  • Comment の各ビューのリンクを /entries/{entry_id}/comments/{comment_id} にする

前者については、config/routes.rb で :only, :except オプションを設定することでリソースに対する操作を制限できるようなのですが、残念ながら実現できませんでした。

後者については、edit_entry_comment_path, entry_comments_url ヘルパーなどを利用すると /entries/{entry_id}/comments に対するリンクを作成できるようです。

認証

続いて、認証について考えてみましょう。「ログインしたユーザのみリソースの参照を許可する」という動きは、今日の Web アプリケーションではごく一般的です。

Rails では restful-authentication というプラグインを利用するのが一般的なようですが、このプラグインはセッションを利用して認証を行います。

セッションが悪いということはないですし、実装方法のひとつとして検討の余地はあると思うのですが、前述したように REST の特徴のひとつはステートレスであるため、REST に従うなら、セッションを使用しない認証方法が必要です。

そこで、別の認証方法について調べたところ、ドキュメントの「Action Controller Overview」の「10 HTTP Authentications」で HTTP による認証を利用した方法が紹介されていました。

上記ドキュメントに掲載されている Basic 認証を用いたコントローラの例は以下のとおりです。

class AdminController < ApplicationController
  USERNAME, PASSWORD = "humbaba", "5baa61e4"

  before_filter :authenticate

private
  def authenticate
    authenticate_or_request_with_http_basic do |username, password|
      username == USERNAME &&
      Digest::SHA1.hexdigest(password) == PASSWORD
    end
  end
end

各リクエストのメソッドが実行される前に、before_filter を使用して Basic 認証のパラメータを検証しています。RESTful な Web アプリケーションを標榜するならば、こちらの認証方法がより適切と言えるでしょう。

このように Rails では、現在 WWW で使用されている一般的な認証方法を一通り実現することができます。しかし、Rails の認証には以下の課題があると筆者は考えています。

  • リソースの特定のアクションに対してのみ認証が必要な場合の実装方法
  • Web アプリケーションにおける認証が必要なリソース一覧の取得

フィルタを使って認証を行った場合、すべてのアクションに対して認証が行われるため、前者に対応することができません。現状では、認証メソッドかアクションメソッドのいずれかで、認証が必要なリクエストかどうかを判断するしかないと思うのですが、もう少し良い解決方法を模索したいところです。

後者は「設計の可視化」という観点から重要です。特定のファイルで認証が必要なリソースを定義できれば、保守性が向上し、統合開発環境によるサポートも受けやすくなります。また、コントローラの役割はモデルとビューの調整であり、認証ではありません。このような観点から、認証の設定はフレームワークの構成ファイルなどで行う方がよいと筆者は考えています。

これらの課題に対して、筆者が考えた解決案は以下のとおりです。

  • 認証の設定を config/routes.rb または config/environment.rb で行えるようにする
  • 認証の設定をアクション単位で行えるようにする

今回は残念ながら実装までは手が回りませんでしたが、機会があったら挑戦したいと思います。

おわりに

今回は Rails 使った RESTful な Web アプリケーションの実装を一通り見ました。URI マッピングや関連するリソースをどのように表現するかという視点は非常に参考になりました。

REST の要点のひとつとして、「HTTP を理解し、正しく活用しよう」ということが挙げられると筆者は考えています。TCP 層のプロトコルの中で、最も利用され、かつ、最も正しく利用されていないプロトコルが HTTP と言えるのではないでしょうか。

そういう筆者も REST を知るまで、「特定のリソースを削除するために HTTP の DELETE メソッドを使用する」という発想はまったくありませんでした。今回、REST について調べてみて、HTTP というプロトコルを考え直す良い機会になりました。

次回は 第 2 回 で見送ったバリデーションの実装を行います。

使用したソフトウェアのバージョン

Ruby on Rails2.3.2

参考文献

トラックバック(0)
  • このブログ記事のトラックバックURL:
コメント