ビューで似たような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 %>
今度はコントローラの話。
例えば、
@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 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) ========================================
テーブル定義は、以下のようになった。

post_id というのが、reference の部分。
この Comment に紐付く Post のIDが格納される。
前に説明したように、Rails で使用するDBは必ず id という Primary Key を持っている。
このルールがあるおかげで、モデル間の参照はこのidさえ持っていればいい。
複合キーなどを参照キーにするのは、百害あって一理無し。Rails ではそんな無駄なことはしない。
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 ファイルの定義は以下のようになっている。
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 は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 の謎に迫るとしよう。
まずは、一覧をリストアップする。
| 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にはいくつか難点はあるが、
このシンプルなコードは非常に魅力的なのでまだ見放すのは早い。