2010年12月14日火曜日

Ruby on Rails3で出納帳を作ろう・その肆

前回scaffold で作成した Category の View 部分(app/views/categories/ ディレクトリ以下)を、一定の水準まで日本語化することができました。しかし、新規作成したときや編集・保存したときの「Category was successfully created/updated.」などのメッセージは、 View のどこにもありませんでした。また、新規作成・編集したときに、さまざまな情報を管理するならまだしも、 name カラムしか持たないレコードをいちいち確認(表示)するのは冗長です。新規作成・編集したあとは、すぐにリスト表示に戻ってもらえないものでしょうか?

これらを解決するのは MVC (Model View Controller) のうち Controller の役割です。 scaffold は、モデル・ビュー・コントローラのすべての土台(と他にもいろいろ)を自動生成してくれるので、これらの要望はわずかな修正をするだけで実現させることができます。

では、いつものようにサーバを起動して作業していきましょう。

$ cd $(PATH_TO_WORK_DIR)/receipt
$ ./script/rails s
=> Booting WEBrick
=> Rails 3.0.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2010-12-14 11:34:13] INFO  WEBrick 1.3.1
[2010-12-14 11:34:13] INFO  ruby 1.8.7 (2010-06-23) [i686-linux]
[2010-12-14 11:34:18] INFO  WEBrick::HTTPServer#start: pid=9997 port=3000





MVCにおけるコントローラというのは、モデルがデータ、ビューがHTMLだとすれば、まさにコードの塊と言えます。塊といっても臆することはありません、何と言っても ruby なのですから書く行はわずかで済みます。

さっそく CategoriesController を見ていきましょう。 app/controllers/ にある categories_controller.rb がコントローラです。

app/controllers/categories_controller.rb:
class CategoriesController < ApplicationController
  # GET /categories
  # GET /categories.xml
  def index
    @categories = Category.all


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


  # GET /categories/1
  # GET /categories/1.xml
  def show
    @category = Category.find(params[:id])


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


  # GET /categories/new
  # GET /categories/new.xml
  def new
    @category = Category.new


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


  # GET /categories/1/edit
  def edit
    @category = Category.find(params[:id])
  end


  # POST /categories
  # POST /categories.xml
  def create
    @category = Category.new(params[:category])


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


  # PUT /categories/1
  # PUT /categories/1.xml
  def update
    @category = Category.find(params[:id])


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


  # DELETE /categories/1
  # DELETE /categories/1.xml
  def destroy
    @category = Category.find(params[:id])
    @category.destroy


    respond_to do |format|
      format.html { redirect_to(categories_url) }
      format.xml  { head :ok }
    end
  end
end

わずかって言ったじゃん!」というツッコミは当然かも知れません。これはあくまで scaffold で自動生成されたコントローラに過ぎないので、ある程度の状況にも耐えられるように、また、改変するときにわかりやすいように、汎用的なコーディングや、コメントなどが残されているのです。また、Rails3より前のバージョンと互換性、可読性を保つために、わざと冗長な書き方をしている部分もあります。

さて、しかし、このコードはどこがどうなっているのでしょう?

鋭い人なら、各メソッドがURIに対応していることに気が付くかも知れません。ここで定義されているメソッドはズバリ、 http://localhost:3000/categories/ なら CategoriesController#index メソッド、 http://localhost:3000/categories/1 なら CategoriesController#show メソッド、というようにルーティングが施されています('1'というのは id=1 のレコードを show する、という意味)。詳しくはコード内のコメントを参照してもらうとして、RailsではかつてのCGIのように /categories_controller?method=show&id=1 などという暗号めいた呼び出しを頭から否定して、直感的な経路にするべし、という設計思想を持っています。

それでも、初めて見る人には意味不明なコードの連続でわかりづらいですね。そこで、全部消してしまいましょう。と言ってもそれでは何もできなくなってしまうので、メソッドを一から作り直していくことにします。

categories_controller.rb:
class CategoriesController < ApplicationController
  # GET /categories
  def index
    @categories = Category.all
  end
end

まずは CategoriesController#index メソッドを再定義しました。わずか一行だけですが、 http://localhost:3000/categories にアクセスしてみると動きます(もちろん「新規作成」リンクをクリックしたりすれば、NoMethodErrorが返ってくるわけですが)。

何が起こっているのでしょうか。

Category というのはモデルクラスで、クラスメソッドである all を呼び出すと categories テーブルにあるすべてのレコードが、このクラスのインスタンスの配列となって返ってきます。実体は app/models/category.rb で、見てみるとたった2行、しかも何のメソッドもないクラスがひとつ定義されているだけです。なぜ Category.all が動くのか、なぜ Category#name などカラム参照メソッド(前回の記事参照)が動くのか、それは ActiveRecord::Base というクラスを継承しているからで、この親クラスがモデルクラスのあらゆる仕事を引き受けてくれています。つまり、データベースへの接続だとかクエリだとかは、ほとんどまったく考えなくてもいいのです。

そして、その配列を @categories というインスタンス変数へ代入しています。それだけです。それだけですが、このインスタンス変数は View (= app/views/categories/index.html.erb) から参照できるので、あとは View に任せてしまえばいいわけです。

モデル・ビュー・コントローラが、それぞれ独立しつつも、互いに関連して動作している感覚がわかってきたのではないでしょうか。ブラウザからのアクセスで、まずコントローラが呼び出され、コントローラが何かしら作業(モデルからデータを持ってきたり)し、処理がビューに渡って描画される。 MVC パターン開発のコツが少し見えてきました。

しかし、元の index メソッドの定義を見てみましょう。

CategoriesController#index:
  # GET /categories
  # GET /categories.xml
  def index
    @categories = Category.all


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

@categories = Category.all は共通していますが、そのあとの respond_to ブロックは何でしょう?

これについては(2006年当時の記事ですが)かの有名なヽ( ・∀・)ノくまくまー(2006-03-17) 優しいRailsの育て方に詳しく解説されています。要約すると、ターゲットはブラウザだけじゃなくて、xmlで出力したり、JavaScriptやRSSでも送信することができて、かつ、それらの入力もできるよ、ということのようです。さらに、入力されたデータをあたかもHTMLフォームから送られてきたように変換する機能もついているとのことで、これを利用すれば、特別なコーディングをしなくてもAPIが作れてしまうというわけですね。オフラインで動作するクライアントも作れそうです。

ということをいま調べて初めて知ったんですが、Rails1.1からの機能のようですねー。これを利用しない手はないでしょう。

というわけで、先程定義した index メソッドに respond_to を付け加えてやります。

CategoriesController#index:
  # GET /categories
  def index
    @categories = Category.all


    respond_to do |format|
      format.html # View (index.html.erb) に処理が渡る
      format.xml  { render :xml => @categories } # /categories.xml で XML が出力される
    end
  end

以下のようなコマンドで確認することができます。

$ wget -O - http://localhost:3000/categories.xml

しかし、Rails3ではこれを「冗長である」と判断して、rubyist 特有の怠け癖というか、「もっと簡潔に!」が実現して、 respond_to はマクロとしても動作するようになりました。次のように簡略化してコーディングすることができます。

class CategoriesController < ApplicationController
  respond_to :html, :xml


  # GET /categories
  def index
    @categories = Category.all
    respond_with @categories
  end
end

こうすることで、 respond_to マクロで指定された形式を respond_with メソッドでまとめて出力できるようになりました。もちろん従来の respond_to メソッドの使い方も可能なので、「このメソッドはXML出力したくないな」などという場合は、 respond_with ではなく respond_to を使う、というようなことも可能です。

続いて、他のメソッドも定義していきましょう。次は new メソッドです。

CategoriesController#new:
  # GET /categories/new
  def new
    @category = Category.new
    respond_with @category
  end

これで「新規作成」リンクがクリックできるようになりました。

@category には Category クラスの新しいインスタンスを代入してやります。これはフォームとの兼ね合いで必要なことなのですが、デフォルトの値を指定することもできます。例えば

@category = Caregory.new :name => 'hoge'

としたり

@category = Category.new
@category.name = 'hoge'

などとしてやると、 new メソッドが呼び出されてフォームがレンダリングされたとき、入力フォームに「hoge」という文字列が必ず入るようになります。

ここで「作成」をクリックすると、ブラウザ上で The action 'create' could not be found for CategoriesController というようなエラーメッセージが表示されます。 action というのはメソッドのことで、つまり「create というメソッドがないよ」と怒られているわけですが、ここでのURLに注目してください。 /categories になっているのに、どうして index メソッドではなく、 create メソッドが呼び出されようとしているのでしょうか?

それは、Railsがバージョン1.2の頃から REST という考え方を導入したからです。単純に言えば、 HTTP にはリクエストメソッドに GET, POST, PUT, DELETE の4つがありますが、うち後者2つはブラウザでサポートされていません。しかし、Railsのフォームに hidden フィールドを設置することで、Rails側が PUT と DELETE を判別できるようになります。

何の役に立つのか? それは、データベースの CRUD のうち、GET=READ, POST=CREATE, PUT=UPDATE, DELETE=DELETE というように関連付けることによって、URLにアクション(メソッド)名を表示させず、綺麗なルーティングで処理を行なって RESTful なアプリケーションの開発を支援しよう、ということなのです。

つまりRails1.2以降では、/categories を GET でリクエストした場合は index アクションを、 POST でリクエストした場合は create アクションを、自動的に呼び出すようになっています。以前はコントローラ内でGETかPOSTか判断して、GETならフォームをレンダリングし、POSTならデータベースに保存し、というようなことをしていたのですが、この思想を導入したことにより、メソッドの名前を決められたものにしておくだけで、HTTPメソッドの判別をする必要がなくなったということです。

RESTという言葉には非常に広い意味があてがわれていて、ここですべてを説明するのは不可能ですし、RailsはRESTをRailsなりに導入したというだけで、RESTについての知識を深める必要は特にありません。ただ、「HTTPメソッドによって呼び出されるアクションの名前が決まっている」ということを、今は覚えておけば大丈夫です。

実際に link_to メソッドにRESTfulな経路を指定したいというようなときは、いずれこの講座で嫌でもやらなきゃならないと思うので、それはその機会に譲りましょう。

話を戻して、メソッド改めアクションを定義していきます。次は、それでは create アクションを実装してみましょう。

CategoriesController#create:
  # POST /categories

  def create
    @category = Category.new params[:category]
    if @category.save
      flash[:notice] = "カテゴリ #{@category.name} は正常に作成されました。"
      respond_with @category do |format|
        format.html { redirect_to categories_path }
      end
    else
      respond_with @category
    end
  end


Category.new がハッシュ値を取れることは先ほどちらっと触れました。フォームに入力されたデータはすべて params[:category] に格納されているので、丸投げするだけでデータの入力されたオブジェクトを作ることができます。が、この時点では保存されていません。Category#save を明示的に呼び出さなければ保存されないのです。

Category#save は保存できたかどうかを真偽値で返すので、正常に保存できた場合は flash[:notice] にメッセージを代入するようにコーディングしています。

flash[:notice] というのは、ビューで一度だけ使われる使い捨てのメッセージボックスのようなものです。読み出し方法は後で説明します。

respond_with @category に、今度はブロックが与えられました。これは HTML 出力するときのみ categories_path へリダイレクトするように設定するためです。


categories_path というのは、先述の REST における GET 、すなわち /categories というURLを得るためのもので、なぜ '/categories' と直接指定してはいけないのかというと、プログラミングのお約束というか、経路が変更される可能性があるから、とか、URL dispatchするときに後で大変だから、とか、とにかくそんな感じの理由だと思います。気持ち的にもリテラルはなるべく入れたくないところですね。

ちなみに、 categories_path がやっぱりわかりづらい、という人には redirect_to :action => :index なんて書き方もあります。個人的には categories_path という RESTful なフィーチャーがせっかく提供されているのだからそちらをお勧めします。

と、なにげなく書いてしまいましたが、新規作成したあとにリスト表示に戻るようにコーディングしました。 categories_path = index アクションがそれですね。名前しか保存していないレコードをわざわざ表示することもないので、カテゴリを管理する場合は index アクションへ直接飛ばしてしまったほうが、ストレスにならないだろうという判断からです。

しかし、試しにこれで作成してみると、確かにデータは保存されてはいるのですが、 flash[:notice] に指定した文字列がページのどこにも表示されません。

それも当然で、ビューである index.html.erb には flash[:notice] を読み出すコードが含まれていないからです。ということで、追加しましょう。 app/views/categories/index.html.erb の好きなところ(個人的には <h1> タグの下)に以下の行を挿入します。

<p id="notice"><%= notice %></p>

もう一度なにかカテゴリを作成してみると、ちゃんと上部にメッセージが表示されているのがわかります。さらに、リロードするとメッセージはもう表示されません。 flash[:notice] はこんな使い方をするものなのです。

また、正常に保存できなかった場合は単に respond_with @category としています。これについては次回説明します。

次は「表示」である show アクションを実装しましょう。慣れてきた人なら、何をするか大方の見当がつくかも知れません。

CategoriesController#show:
  # GET /categories/1
  def show
    @category = Category.where(:id => params[:id]).first
    respond_with @category
  end

REST に従い、 show アクションは /categories/1 などのリクエストのときに呼び出されます。1というのはレコードの id カラムのことです。これは params[:id] によって得ることができます。

では、Category.where から始まる一連の呪文は何でしょう? これはRails3においてもっとも変わったと言われるActiveRecordの新機能で、端的に言えばクエリを ruby で表現した DSL なのですが、その挙動は非常に洗練されたものです。

まず、 Category.where はハッシュを取り、keyに指定されたカラムとvalueを組み合わせたSQL文のWHERE句を生成します。しかし結果がいくつであろうと配列で返ってくるので、 first などで表に出してやる必要があります。まぁ、ここまでは普通の出来事です。

さらに、 Category.where の戻り値は関連オブジェクトであり、実行可能なので、メソッドチェインでどんどん条件を追加することができます。例えば Address という住所録テーブルがあるとして:

Address.where('name like ?', '鈴木%').where('birthday < ?', 30.years.ago.to_date).limit(10).order(:id)

など、書いてて楽しいです。
何より凄いのは、この呼び出しが遅延評価されているというところです。メソッドチェインするからには戻り値(ActiveRecord::Relationのインスタンス)に対して処理をしていくわけですが、このように自身を返すようなメソッドチェインは概して処理が遅くなりがちです。しかし遅延評価させることによって、最後の呼び出しの時点で初めてSQL文を発行することができ、動的かつシンプルで強力なクエリを実現させています。

これまでは find メソッドを使っていたところをこのように置き換えることで、例えば複雑な検索条件を指定したいときなどに、泥臭い文字列処理をしたり、またSQL文を長々と書いたりする必要がなくなりました。またひとつRailsが楽しくなったという感じですが、 ActiveRecord の後ろに控えている Arel ライブラリは数学的関係代数を本格的に実装しているので、NoSQLミドルウェアなど各種データベース群雄割拠の時代にふさわしい革新的なポストにあると個人的に思ってます。

※とかエラそうなことホザいてますがよくわかっていないので間違っていたらツッコミもしくは補完してください…。

さて、次は edit アクションです。 show アクションとほとんど変わりありません。

CategoriesController#edit:
# GET /categories/1/edit
  def edit
    @category = Category.where(:id => params[:id]).first
  end

respond_with が無いのは、将来的に API として使用したいと思ったときに、フォームをレンダリングするだけの edit アクションには XML などを出力する意味がないからです。というか、 scaffold したコードに respond_to がなかったので、勝手にそう思っています。

new アクションには respond_to がついているところを見ると、 new アクションの場合はデータ形式を得るために必要だからでしょう。

続いて update アクションです。

CategoriesController#update:
  # PUT /categories/1

  def update
    @category = Category.where(:id => params[:id]).first
    if @category.update_attributes params[:category]
      flash[:notice] = "カテゴリ #{@category.name} は正常に更新されました。"
      respond_with @category do |format|
        format.html { redirect_to categories_path }
      end
    else
      respond_with @category
    end
  end

update_attributes はハッシュ値を取り、そのまま更新するメソッドです。

しかし、 flash[:notice] = ... や respond_with ...create アクションとまったく同じですね。こういうのは DRY (Don't Repeat Yourself) に反しますので、新しくプライベートなメソッドを定義しましょう。

private
def redirect_to_index category, message
  respond_with category do |format|
    format.html { redirect_to categories_path, :notice => message }
  end
end

redirect_to メソッドに :notice 属性を与えることで、 flash[:notice] = ... という一行を省略することができます。これを、 create アクションと update アクションに採用してやりましょう。

CategoriesController#create:

  def create
    @category = Category.new params[:category]
    if @category.save
      redirect_to_index @category, flash[:notice] = "カテゴリ #{@category.name} は正常に作成されました。"
    else
      respond_with @category
    end
  end


CategoriesController#update:

  def update
    @category = Category.where(:id => params[:id]).first
    if @category.update_attributes params[:category]
      redirect_to_index @category, flash[:notice] = "カテゴリ #{@category.name} は正常に更新されました。"
    else
      respond_with @category
    end
  end


これがRails3流なのかどうかわかりませんが、Rails2系まではこれが良しとされていたようです。真の DRY についてはエロい人の朗報を待ちましょう。

さあ、いよいよ最後のアクション、 destroy の実装です。

CategoriesController#destroy:
  # DELETE /categories/1

  def destroy
    @category = Category.where(:id => params[:id]).first
    if @category.destroy
      redirect_to_index @category, flash[:notice] = "カテゴリ #{@category.name} は正常に破棄されました。"
    else
      respond_with @category
    end
  end


destroy メソッドはわかりやすいですね。
ここにも respond_and_redirect_to_categories_path メソッドが出てきました。

というわけで、無事にスクラッチから CategoriesController を定義することができました。
冗長になるので全文は載せませんが、動いているなら問題ないでしょう。冒頭で「わずかな修正だけで…」とか書いていましたがあれはウソでした。ごめんなさい。

ちなみに、

$ rake stats

とするとアプリケーションのいろいろな情報を手に入れることができます。コントローラのコードの行数は…ボクの場合 67 となっていました。あなたの場合はどうでした?

さて、コントローラをいじくってさんざんカテゴリを追加・修正・削除しているうちに、大切なことに気がつきました。「あってはならないことが起こっている…」次回はそれを解決するために、ちょっとモデルに近付いてみましょう。

ではまたっ!

0 件のコメント:

コメントを投稿