minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

render

ビューで似たようなHTMLを何度も記述する場合、それを
テンプレートファイルとして分離することができる。
まぁこれはどのフレームワークにでもある当たり前の機能。

新規投稿画面を以下のように変更してみる。

app/views/posts/new.html.erb

<h1>New post</h1>
<%= render :partial => "form" %>
<%= link_to 'Back', posts_path %>

2行目がその部分。

render :partial => "テンプレート名"

とする。
実際のテンプレートファイルは、先頭に _ を付けて以下のように。

app/views/posts/_form.html.erb

<% form_for(@post) do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </p>
  <p>
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

before_filter

今度はコントローラの話。
例えば、

@post = Post.find(params[:id])

という処理がコントローラの中に4回記述されている。
こんな簡単な文でも、何度も記述するのは良くない。

Rails で最も大事な概念。それがDRY、"Don't Repeat Yourself" だ。
複数箇所登場する同じ記述は、必ず一箇所にまとめる。
今回の場合は、フィルタというものを使う。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_filter :find_post,
    :only => [:show, :edit, :update, :destroy] 
  
  private
    def find_post
      @post = Post.find(params[:id])  end 

これで、show, edit, update, destroy メソッドの呼び出し前に
find_post メソッドが呼び出されることになる。

generate model

generate scaffold では Model, View, Helper, route など様々なものが作成された。
今度は Model だけを単独で追加してみる。
Comment モデルを、以下のような感じで。

% script/generate model Comment commenter:string body:text post:references
    create  app/models/comment.rb
    create  test/unit/comment_test.rb
    create  test/fixtures/comments.yml
    create  db/migrate/20090323143442_create_comments.rb

post:references というのが、今回のキーポイント。
これは、Comment が親の Post を参照に持つという意味。
つまり、Post と Comment の関係が 1対n になる。
モデルとDB定義を見てみよう。

app/models/comment.rb

class Comment < ActiveRecord::Base
  belongs_to :post
end

db/migrate/20090323143442_create_comments.rb

class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :post
      t.timestamps
    end
  end
  def self.down
    drop_table :comments
  end
end

belongs_to, references など、それらしい記述が並ぶ。
ではDBを作成する。

% rake db:migrate
(in /mnt/var/prog/rails/sample2)
==  CreateComments: migrating =================================================
-- create_table(:comments)
   -> 0.0074s
==  CreateComments: migrated (0.0081s) ========================================

テーブル定義は、以下のようになった。

db2.png

post_id というのが、reference の部分。
この Comment に紐付く Post のIDが格納される。
前に説明したように、Rails で使用するDBは必ず id という Primary Key を持っている。
このルールがあるおかげで、モデル間の参照はこのidさえ持っていればいい。
複合キーなどを参照キーにするのは、百害あって一理無し。Rails ではそんな無駄なことはしない。

Post モデル

generate によって作成された Comment モデルは、Post を参照している。
しかしまだこのままでは、Post から Comment への参照が存在しない。

app/models/post.rb

class Post < ActiveRecord::Base
end

このように、Post モデルは空のままだ。
ここに1文追加して以下のようにする。

app/models/post.rb

class Post < ActiveRecord::Base
  has_many :comments
end

これで、Post が複数の Comment を持つことが明示された。

route

現在、route ファイルの定義は以下のようになっている。

config/routes.rb

map.resources :posts

これは generate scaffold したときに追加された1文。
今まで謎のまま放置しておいた posts_path, new_post_path などだが
どうやらこの1文がこれらの変数を定義しているらしい。
その証拠に、この1文をコメントアウトすると上記の箇所でエラーになる。

とりあえず、その謎はまだ放置したままで先に進む。
この1文を、以下のように変更する。

config/routes.rb

map.resources :posts, :has_many => :comments

これが何の効果を及ぼすのか不明だが、後で有効になってくるらしい。
まぁいい。まずは先へ進もう。

generate controller

generate controller は2度目の登場。
最初に登場したときは、以下のような感じだった。

% script/generate controller home index 

これで home コントローラ、indexアクションおよびViewが作成された。
今回は、以下のようにする。

% script/generate controller Comments index show new edit
    create  app/controllers/comments_controller.rb
    create  test/functional/comments_controller_test.rb
    create  app/helpers/comments_helper.rb
    create  app/views/comments/index.html.erb
    create  app/views/comments/show.html.erb
    create  app/views/comments/new.html.erb
    create  app/views/comments/edit.html.erb

Comments コントローラ、index / show / new / edit アクションおよびViewの作成。
コントローラにはまだ何もない。

app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  def index
  end

  def show
  end

  def new
  end

  def edit
  end

end

ビューもまだ空。

app/views/comments/index.html.erb

<h1>Comments#index</h1>
<p>Find me in app/views/comments/index.html.erb</p>

app/views/comments/show.html.erb

<h1>Comments#show</h1>
<p>Find me in app/views/comments/show.html.erb</p>

app/views/comments/new.html.erb

<h1>Comments#new</h1>
<p>Find me in app/views/comments/new.html.erb</p>

app/views/comments/edit.html.erb

<h1>Comments#edit</h1>
<p>Find me in app/views/comments/edit.html.erb</p>

では、コントローラに一気にロジックを書く。
ここは長いしあまり意味も無いので、原文を参照。
http://guides.rubyonrails.org/getting_started.html

app/controllers/comments_controller.rb

詳しい内容は後で追っていくが、一つ。
post_id と id という二つの変数が使われている。
前者は、Commentが所属する親PostのID、後者はCommentそのもののIDだ。
Postのコントローラでは、IDしか使っていなかった。

では、View の方も一気に作成していく。これも原文参照。

views/comments/index.html.erb
views/comments/new.html.erb
views/comments/show.html.erb
views/comments/edit.html.erb

さて、これで Comment の方は一段落。
ここからは Post の方に手を付けていく。
まずは Post の Show View。

app/views/posts/show.html.erb

ここに、コメントの表示箇所を追記する。例によって原文を参照。
注目すべきは2箇所。

@post.comments.each
post_comments_path(@post)

前者は、この Post に関連付けられた Comment をループさせる部分。
先ほど、モデルを以下のように修正している。

app/models/post.rb

class Post < ActiveRecord::Base
  has_many :comments
end

しかしまだロジックは入れていないので、comments は常に空の状態だ。

後者は、この Post に関連付けられた Comment を表示させるためのURL。
これは以下のように展開される。

http://localhost:3000/posts/2/comments

相変わらずこの展開ロジックが謎だが、まだここは放っておく。
このリンクをクリックすると、Comment コントローラに処理が移る。

Comment コントーラの index アクションを見てみる。

def index
  @post = Post.find(params[:post_id])
  @comments = @post.comments
end

ここの post_id に、2 が展開されているというわけ。
つまり…

http://localhost:3000/posts/2/comments

このURLは、Postの2番目を扱い、その情報を持ってCommentコントローラに
処理を移すということを意味する。
それを指示しているのが、実は先程修正した以下の一文。

config/routes.rb

map.resources :posts, :has_many => :comments

ここで、Post が Comment の子を持つということを示している。
これが無いと post_comments_path(@post) もエラーになるし
http://localhost:3000/posts/2/comments というURLも無効になる。
この場合、Post が comments というアクションを持っていないというエラーが出る。

では、早速この投稿にコメントを付けてみる。
Manage Comments リンクをクリックすると、Commentコントローラのnewアクションに処理が移る。

def new
  @post = Post.find(params[:post_id])
  @comment = @post.comments.build
end

注目すべきは build メソッド。Post.new は、以下のような定義だった。

def new
  @post = Post.new
end

今回、Commentをそのままnewしてはいけない。
なぜなら、そのCommentは親となるPostを持つからである。
親のIDが post_id 変数に入っているので、そのPostをまずfindし
そのpostインスタンスが持つ comments をビルドする。
こうすることによって、この @comment は @post の子であるということが認識される。

しかし勘のいい人は、なぜここで @post.comments が nil でないか
不思議に思うだろう。これについては、いずれ解明していく。

今回の form_for 定義は、以下のようになっている。

views/comments/new.html.erb

<% form_for([@post, @comment]) do |f| %>

form_for の引数に @post と @comment の両方が指定されている。
では画面に戻り、適当なコメントを入力してSUBMITする。
Comment.create を見てみよう。

app/controllers/comments_controller.rb

def create
  @post = Post.find(params[:post_id])
  @comment = @post.comments.build(params[:comment])
  if @comment.save
    redirect_to post_comment_url(@post, @comment)
  else
    render :action => "new"
  end
end

今度も build に注目。Post.create と比較してみよう。

app/controllers/posts_controller.rb

def create
  @post = Post.new(params[:post])
  ...

ここでも、new の代わりに build を使っている。
最後は、post_comment_url(@post, @comment) の部分。
これは、以下のURLに展開される。

http://localhost:3000/posts/2/comments/1

2番目のPostが持つ、1番目のCommentという意味。
これは、Comment.show アクションが処理することになる。

route

では以上を踏まえ、今まであえて避けてきた route の謎に迫るとしよう。
まずは、一覧をリストアップする。

URL Http Method コントローラ アクション名 変数定義
/posts GET Post index posts_path
/posts/new GET Post new new_post_path
/posts POST Post create form_for(@post)
/posts/NO GET Post show @post
/posts/NO/edit GET Post edit edit_post_path(@post)
/posts/NO POST Post update form_for(@post)
/posts/NO/comments GET Comment index post_comments_path(@post)
/posts/NO/comments/new GET Comment new new_post_comment_path(@post)
/posts/NO/comments POST Comment create form_for([@post, @comment])
/posts/NO/comments/CNO GET Comment show post_comment_path(@post, @comment)
/posts/NO/comments/CNO/edit GET Comment edit edit_post_comment_path(@post, @comment)
/posts/NO/comments/CNO POST Comment update form_for([@post, @comment])

さぁ、何となく見えてきただろうか(笑)。
Postコントローラの割り当ては、

map.resources :posts

によって定義された。
PostおよびCommentコントローラの割り当ては、

map.resources :posts, :has_many => :comments

によって定義された。
ここら辺を詳しく追っていくと奥が深そうなので、ここでは定義を載せるだけに留めておく。

以上、かなりざっとではあるがチュートリアルを終了する。

感想

ここまでチュートリアルをやってきて、できたものはほんの小さなものである。
もちろんこれを一から作るのは大変なことなのだが
じっくり内容を理解しながら進めていくと一から作るのと同じくらいの時間が掛かる。

そして、実際のWebアプリを作るのには他にもたくさん必要なことがある。
認証をしたい、ファイルを添付したい、等々。
そのようなことをやりたいとき、それを実現するためにはどうしたらいいかが
すぐにはわからないのが現状だ。

フレームワークなら必ず陥る「作成は簡単だが、拡張は難しい」という点は
Rails の大きな不安材料に見える。ドキュメントが弱い。
もっと普及してこれらのサンプルが簡単に揃うようになればいいが
Python の普及などもあり、Rails を使う場面というのは今のところ少なそうだ。

あと、HTMLコード内にRubyスクリプトが埋め込まれているのも良くない。
これは過去にJSPが反省した点であり、これではビューの分離ができていない。
Java + Velocity, Python + KID の方が優れている。

というわけでRailsにはいくつか難点はあるが、
このシンプルなコードは非常に魅力的なのでまだ見放すのは早い。