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

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

前回の記事では、Rails2.0のscaffoldをベースに、RESTfulなdel.icio.usクローンのブックマークアプリケーションを作成してみました。今回は前回作成したアプリケーションをRails2.0の機能を用いて改良していきたいと思います。

リソース設計

前回作成したブックマークアプリケーションは、RESTの統一インターフェースにもとづいた、以下のリソースを持ちます。

機能HTTP MethodURI
リンクの作成POST/links
リンクの表示GET/links/:id
リンクの変更PUT/links/:id
リンクの削除DELETE/links/:id

つまり、現在のminiciousはログインした本人のリンクのCRUDが可能な、それしかできないアプリケーションです。

ソーシャルブックマークと名乗っているにもかかわらず、一人の世界だけで完結しているのはあまりに寂しいものです。他のユーザのリンクも閲覧可能にしましょう。ただし、自分以外のユーザのリンクを編集できるのは問題ですから、作成、変更、削除が可能なのは自分のリンクだけとします。

これらを踏まえて、提供するリソースを拡張してみます。

機能HTTP MethodURI
リンクの作成POST/:username/links
リンク一覧表示GET/:username/links
リンクの表示GET/:username/links/:id
リンクの変更PUT/:username/links/:id
リンクの削除DELETE/:username/links/:id
タグ一覧表示GET/:username/tags
タグの表示GET/:username/tags/:id

Routing map.resourceふたたび

前説で設計したリソースに対する、RailsのRouting設定はどうなるでしょうか?

具体的なコードに入る前に、RailsのRESTful Routingを作成する、map.resources, map.resouceの機能について再度まとめておきます。

特に、map.resouces、map.resouceでRESTfulなCRUDが作成されることはこれまで何度が見てきました。ここでは、これらのメソッドが持つオプションについて見ていきましょう。

:path_prefix

リソースがネストする場合に、リソースの前に付くパスを指定します。前節のリソース設計で、リンクリソース、タグリソースの前に、ユーザ名のパスを前置するよう修正しました。そういった場合のresoucesは以下のようになります。

map.resouces :links, :path_prefix => ':username'

ここで指定した、path_prefixはコントローラやビュー内でパラメータとして取得できます。

:has_many, :has_one

Routingのネストを行い、従属リソースを表現します。:has_manyオプションは複数のリソースを表現し、:has_oneは単一のリソースを表現します。

今回作成しているブックマークアプリケーションでは「ユーザ」をリソースとして提供していませんが、⁠ユーザ」のCRUDをRESTful Webサービスで行うアプリケーションも考えられます。そういった場合のRouting定義は以下のようになります。

:has_manyオプション
map.resources :users, :has_many => [:links, :tags]

ユーザリソースに、リンクとタグを従属させています。この場合のリソースは以下のように表現されます。

リソース抜粋
機能HTTP MethodURI
リンクの作成POST/users/:id/links
リンクの表示GET/users/:id/links/:id
タグの表示GET/users/:id/tags/:id
例:リンクの表示
GET /users/1/links/6.xml

:member

CRUD以外、すなわち作成、参照、更新、削除以外の処理を行いたい場合、map.resoucesで、:memberオプションを指定します。:memberオプションには、アクション名とHTTP MethodのHashを指定します。

例として、リンクを非表示に設定するケースを考えます。リンクの非表示を、hideカスタムアクションへのPUT Methodで表現すると決定した場合、以下のように設定します。

:memberオプション
map.resouces :link, :member => {:hide => :put}
URI例
PUT /links/1/hide

なお、このURIにはhideという「動詞」が入っているため、RESTの統一インターフェースの原則的にはあまり望ましくありません。

miniciousのRouting定義

これらの機能を用いた、前節のリソース設計に対するRouting定義は以下のようになります。

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

ActiveResource

ここで、ActiveResourceについて紹介しておきます。

ActiveResource とは、Rails2.0からRails Coreに導入された、RailsのRESTful routingと、XML表現を解釈するパッケージです。ActiveRecord は、RDBとRubyのオブジェクトとをマッピングするRailsのパッケージですが、それに対してActiveResourceは、XMLや、JSONで表現されたRESTfulな「リソース」をデータとして扱い、Rubyのオブジェクトとして抽象化します。

また、ActiveResourceの導入と入れ替わりで、RailsでSOAPや、XML-RPCといったWebサービスを管理するパッケージであった「ActionWebService」はCoreから除外されました(別途、gemsでインストール可能です⁠⁠。

ActiveResourceにできること

ActionWebServiceの代わりとして導入されたActiveResourceですが、Webサービスを汎用的に取り扱うActionWebServiceと異なり、その機能は非常に限定されたものです。ActiveResourceにできることは、RailsのRESTful routing規約に準じたWebサービスを解釈し、Rubyのオブジェクトとして処理することです。Rails以外のWebサービスにも利用できますが、Railsと同様のRESTful URIを持つ必要があります。

今回作成したアプリケーションは、RailsのRouting機能(map.resources)を用いて作成したWebアプリケーションであるため、当然ActiveResourceに適合します。

ActiveResouceの利用例

それでは、ActiveResourceを利用してminiciousにアクセスしてみましょう。ここで注意が必要です。現在のminiciousアプリケーション内に、ActiveResourceを利用したクラスを作成すると、ActiveRecordとActiveResourceの命名規約からクラス名の重複が発生します。そのため冗長ではありますが、ActiveResource利用するために、クライアントアプリケーションフォルダをもうひとつ作成します。

% rails minicious-es

つづいて、作成したminicious-esアプリケーション内でActiveResource::Baseを継承したクラスを作成します。設置場所に悩みますが、今回はModel的な利用になるためmodelsフォルダの配下に設置します。

app/models/link.rb
class Link < ActiveResource::Base 
    self.site = 'http://user:user@localhost:3000/'  
    self.prefix = '/user/'
end

これだけで、RESTfulなWebサービスの利用が可能となります。

コードを見ていきましょう。まず、ActiveResouce::Baseの特異メソッド「self.site=」で、Webサービスを提供するURIを指定します。今回作成した、ブックマークアプリケーションでのRESTfulな作成、更新、削除アクセスはBasic認証が必要となっています。そのため「http://」につづいて「ユーザ名:パスワード@」の文字列を追加し、Basic認証を行うことを知らせています。

つづいて、ActiveResouce::Baseの特異メソッド「self.prefix=」で、リンクのCRUDを行う接頭のURIを指定しています。miniciousのリソース設計では、ユーザ名がリンクのURIの前にくるため、prefixに「user」というユーザ名を直接指定しています。なお、prefixは「/」ではじまり「/」で終わる必要があります。

それでは、作成したActiveResouceのLinkクラスを利用してみましょう。ActiveResourceでは、ActiveRecordと同様のメソッドが利用できます。

ActiveRecordActiveResourceURI
Link.find(:all)Link.find(:all)GET http://localhost:3000/user/links.xml
Link.find(1)Link.find(1)GET http://localhost:3000/user/links/1.xml
Link.exists?(1)Link.exists?(1)GET http://localhost:3000/user/links/1.xml
Link.create(condition)Link.create(condition)POST http://localhost:3000/user/links

ユーザ名「user」「http://www.exampl.com/」へのリンクを作成し、タグとして「example」「test」として分類する場合、以下のようになります。

script/consoleでの実行例
>> Link.create({:url=>"http://www.example.com", :tag_names=>"test example"})

=> #<Link:0xb795650c @prefix_options={}, @attributes={"updated_at"=>Tue May 06 14:00:44 UTC 2008, "url"=>"http://www.example.com", "notes"=>nil, "id"=>20, "tag_names"=>"test example", "user_id"=>1, "created_at"=>Tue May 06 14:00:41 UTC 2008}>

いかがでしょうか。こういった形で、簡易にWebアプリケーションにアクセスできるのが、RailsでRESTfulなWebアプリケーションを作成することの醍醐味と言えます。

最終的なアプリケーションコード

ここまでの機能を含む最終的なコードの簡単な解説をしていきます。

モデル

まずは、モデルから解説します。

app/models/link.rb
class Link < ActiveRecord::Base
  has_many :assorts
  has_many :tags, :through=>:assorts
  belongs_to :user

  def tag_names=(value)
    Tag.transaction do
      self.save
      tag_ids = self.tags.map(&:id)
      self.tags.delete(Tag.find(tag_ids)) if self.tags
      tags = value.split(" ")
      tags.each do |t|
        self.tags << Tag.find_or_create_by_name(t)
      end
    end
  end

  def tag_names
    self.tags.map(&:name).join(" ")
  end

  def self.create_with_user(condition, current_user)
    @link = Link.create(condition) 
    @link.user = current_user    
    @link
  end

  def self.find_by_user_name_and_tag_id(user_name, tag_id)
    Link.find_by_sql([<<-SQL, {:user_name => user_name, :tag_id => tag_id}])
      SELECT 
        links.*
      FROM
        users,
        tags,
        assorts,
        links
      WHERE
        users.login = :user_name
      AND
        links.user_id = users.id
      AND
        assorts.link_id = links.id
      AND
        tags.id = :tag_id
      AND
        tags.id = assorts.tag_id
    SQL
  end
end

リンクを管理する、Linkモデルです。

代入メソッドtag_names=で、既存のタグを削除し、新規のタグを作成する付け直しの作業をしています。先にsaveメソッドを呼んでいるのは、has_many: throughで多対多の関連を作成した場合の制限によるものです。

また自身以外のユーザによる変更は発生しないはずですが、念のため Transaction ブロックで囲みトランザクション処理をしています。

取得メソッドtag_namesでは、リンクに関連付けられたタグのリストから、スペースで区切られたタグ名一覧を作成しています。この2点の修正で、Linkモデルにtag_names属性を追加します。

find_by_user_name_and_tag_idでは、ユーザ名とタグIDからリンク一覧を検索しています。この箇所だけではなく、miniciousでは積極的にfind_by_sqlを利用しています。

app/models/tags.rb
class Tag < ActiveRecord::Base
  has_many :assorts
  has_many :links, :through=>:assorts

  def self.find_by_user_name(user_name)
    Tag.find_by_sql([<<-SQL, {:user_name => user_name}])
      SELECT 
        count(tags.name) cnt
        ,tags.*
      FROM
        users,
        tags,
        assorts,
        links
      WHERE
        users.login = :user_name
      AND
        links.user_id = users.id
      AND
        assorts.link_id = links.id
      AND
        tags.id = assorts.tag_id
      GROUP BY tags.name
    SQL
  end
end

タグを管理するTagモデルのfind_by_user_nameメソッドでは、ユーザが持つタグの一覧を検索しています。SQLのCOUNT関数で、ユーザが持つ全リンク内のタグの件数を取得し、⁠cnt」というタグ件数を保持する属性を作成しています。ActiveRecordでは、Rubyのメタプログラミングにより「メソッドが生えてくる」ようなイメージで属性が利用できます。

コントローラ

つづいて、コントローラです。

app/controllers/links_controller.rbから抜粋
class LinksController < ApplicationController

  before_filter :login_required , :only => [:new, :create, :edit, :update, :destroy]

  # 以下省略
end

リンクURIをコントロールするLinksControllerです。before_filterの:onlyオプションで認証を行うアクションの指定を行っています。更新系のアクションにのみ認証を設定しています。

上のコード例以外の修正として、URIの変更にともない、リダイレクトするパスの修正を行っています。

ERb View

最後に、ERbのviewです。

app/views/links/index.html.erb
<table width="100%"> 
<tr>
<% if @links.size > 0 %>
<td width="70%">
<h1>links</h1>
<table>
<% for link in @links %>
  <tr>
    <td><%= link_to link.url, link.url %></td>
    <% if   current_user_id == params[:username] %>
    <td><%= link_to 'edit', edit_link_path(params[:username], link) %> /</td>
    <td><%= link_to 'destroy', link_path(params[:username], link), :confirm => 'Are you sure?', :method => :delete %></td>
    <% end %>
  </tr>

  <tr>
    <td colspan="3"><small><%=h link.notes %></small></td>
  </tr>
  <tr>
    <td colspan="3"><small>to <%=h link.tag_names %></small></td>
  </tr>
<% end %>
</table>
</td>
<% end %>

<% if @tags.size > 0 %>
<td width="30%" valign="top" bgcolor="#dddddd">
<h1>Tags</h1>
<table>
<% for tag in @tags %>
  <tr>
    <td><%= tag.cnt%> <%= link_to tag.name, tag_url(params[:username], tag) %></td>
  </tr>
<% end %>
</table>
</td>
<% end %>

</tr>
</table>
<br />

リンク一覧を表示する、index.html.erbです。

ここではURIの変更に関連するlink_pathの修正に注意が必要です。link_pathでは、path_prefixで指定した:usernameをパラメータから取得し、@linkオブジェクトとあわせて2つの引数を渡しています。また「:method=>:delete」としてHTML Formから疑似HTTP DELETEメソッドを発行するよう指定しています(hiddenフィールドに設定されます⁠⁠。これは、GETとPOSTしかできないWebブラウザ制限へのRESTful URIでの対処法です。更新時、edit.html.erbのform_forでは同様に「:method=>put」を指定しています。

index.html.erbでは、Linkモデルで作成したtag_names属性と、Tagモデルで作成したcnt属性を利用しています。

画面イメージとソースアーカイブ

次の画面は、リンク一覧です。リンク下部にタグの表示と、右のカラムにはタグ一覧が件数とともに表示されています。

リンク一覧
リンク一覧

次の画面は、タグ一覧です。タグ付けされたリンクの一覧が表示されます。

タグ表示
タグ表示

ソースコードアーカイブ

以下が今回改良したminiciousソースアーカイブです。

ちなみに最終段階においてもminiciousにはユーザ管理の画面はありません。今回作成したminiciousは、RESTとRoy Fieldingに敬意を表して、Apache License 2.0としますので、興味を持たれた方は機能追加してみてください。

まとめと展望と次回予告

今回は、Rails2.0の機能を用いてRESTfulにブックマークアプリケーションを拡張しました。

今回作成したアプリケーションは、単体の機能だけみるとあまりに簡素過ぎるアプリケーションです。しかし、RESTfulなアプリケーションを作成する最大の魅力は、統一インターフェースによる拡張性や接続性の拡大によるシナジー効果にあります。

例えば、RESTfulにサービスのインターフェースを統一することで、RESTful Railsアプリケーションであれば同じライブラリで一括して対応可能なことが期待できます。そういったRESTful Railsに対応するRubyにおけるライブラリ例が先に述べたActiveResouceです。

残念ながら、Webブラウザ上で利用可能な、JavaSriptでのActiveResouceに相当するライブラリは現状ありませんが、そういったライブラリを利用する、RESTful RailsへのWebブラウザアドオンなどを考えてみるのも楽しいのではないでしょうか。

Railsによるアプリケーション作成の実例はこの辺にしまして、次回は、Railsで製品レベルのWebアプリケーションを作成する際に重要となるパフォーマンスの問題について考えていきたいと思います。

おすすめ記事

記事・ニュース一覧