めもめも。

Railsで依存関係があるモデルを実装するのに、この記事が大変参考になります。

素晴らしい記事です。ただ残念ながら2007年、当時はRails 2.*ですから、現在とは色々と違っています。

Rails 3.1.1で同じような事をやってみました。

ひとつのPostに複数のCommentが所属する、ブログ風のシステムです。

Postを作る

普通にPostを作ります。

Ruby on Railsを開始。

$ rails new blog
$ cd blog

まずはscaffoldで、モデル、ビュー、コントローラーを、rakeでDBを作成。

$ rails g scaffold Post title:string body:text
$ rake db:migrate

rootにアクセスしたとき、Postの一覧が表示されるようroutingを設定。

config/routes.rb

  # You can have the root of your site routed with "root"
  # just remember to delete public/index.html.
  # root :to => 'welcome#index'
  root :to => 'posts#index'

そこのコメントにある通り、index.htmlを削除する。(削除しないとそれが表示されてしまうので。)

$ rm public/index.html

とりあえずここらで動かしてみる。Railsの簡易Webサーバーを起動する。(Apacheで見られるならそっちでもいい。)

$ rails s

http://192.168.1.1:3000/等をブラウザーで開く。ついでなので幾つかデータを登録しておく。

終わったらサーバーをCtrl-Cで停止。

CommentをPostの配下に作る

さあ本番だ。

Postを外部キーに持つCommentを作成

外部キーにPostを持つCommentを作成。

$ rails g scaffold Comment post:references body:text
$ rake db:migrate

referenceではなくreferencesね。

config/routes.rb

冒頭のこれを、

  resources :comments

  resources :posts

こうする。

  resources :posts do
    resources :comments
  end

モデルも修正する。

app/models/post.rb

class Post < ActiveRecord::Base
end
class Post < ActiveRecord::Base
  has_many :comments
end

これでCommentはPost配下になった。(本当はCommentsのモデルでも指定しなくちゃいけないんだけど、scaffoldがちゃんとやってくれている。)

まだ実装がroutingに沿っていないので、修正が必要。

コントローラーを修正

CommentがPost配下であるからには、Commentの操作の際は必ずPostが存在する。

Commentのコントローラーで、所属するPostをインスタンス変数に呼び出しておく。冒頭でフィルターを追加。

app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_filter :load_post

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

これでPostのインスタンスを得るようになった。続いて、Commentのインスタンスは全てこの@postから得るようにする。

変更前 変更後 数
Comment.find @post.comments.find 4
Comment.new @post.comments.build 2
redirect_to(@comment) redirect_to([@post, @comment]) 2
redirect_to(comments_url) redirect_to(post_comments_url(@post)) 1

例えばこれを、

   def show
     @comment = Comments.find(params[:id])

こうする。

   def show
     @comment = @post.comments.find(params[:id])

ビューを修正

コントローラーの次はビュー。

app/views/comments/_form.html.erb

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

Postはシステムが取得するので、ユーザーは入力しない。この部分は削除。

  <div class=&quot;field&quot;>
    <%= f.label :post %><br />
    <%= f.text_field :post %>
  </div>

app/views/comments/new.html.erb

<%= link_to 'Back', comments_path %>
<%= link_to 'Back', post_comments_path %>

app/views/comments/edit.html.erb

<%= link_to 'Show', @comment %> |
<%= link_to 'Back', comments_path %>
<%= link_to 'Show', [@post, @comment] %> |
<%= link_to 'Back', post_comments_path %>

app/views/comments/index.html.erb

    <td><%= link_to 'Show', comment %></td>
    <td><%= link_to 'Edit', edit_comment_path(comment) %></td>
    <td><%= link_to 'Destroy', comment, confirm: 'Are you sure?', method: :delete %></td>
    <td><%= link_to 'Show', post_comment_path(@post, comment) %></td>
    <td><%= link_to 'Edit', edit_post_comment_path(@post, comment) %></td>
    <td><%= link_to 'Destroy', post_comment_path(@post, comment), confirm: 'Are you sure?', method: :delete %></td>

もいっちょ。

<%= link_to 'New Comment', new_comment_path %>
<%= link_to 'Back', post_path(@post) %> |
<%= link_to 'New Comment', new_post_comment_path %>

app/views/comments/show.html.erb

<%= link_to 'Edit', edit_comment_path(@comment) %> |
<%= link_to 'Back', comments_path %>
<%= link_to 'Edit', edit_post_comment_path %> |
<%= link_to 'Back', post_comments_path %>

最後に、Postの画面からCommentの一覧へのリンクを張る。

app/views/posts/show.html.erb

<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
<%= link_to 'Comments', post_comments_path(@post) %> |
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>

これで一通りの操作が行えるはず。/posts/1/comments/1でコメントが表示されたりする事を確認。

Commentの一覧をPostの参照画面に結合

Postの表示画面にCommentの一覧を埋め込む。(元記事だとPostのviewにComment一覧を直接出力してるけど、こうやって分けた方が良さそう。)

新規作成画面も取り込むので、空のCommentインスタンスを用意しておく。(名前は変えた方が良さそうだなあ。)

app/controllers/posts_controller.rb

  def show
    @post = Post.find(params[:id])
    @comment = @post.comments.build

この空のCommentインスタンスも@post.commentsに含まれるみたいなので、@post.comments.sizeは最小でも1になる。

で、Postの参照画面に、Commentのビューを含める。

app/views/posts/show.html.erb

<%= link_to 'Comments', post_comments_path(@post) %> |
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
<%= render 'comments/index' %>
<%= render 'comments/form' %>

<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>

これはpartialという機能。読み込むファイルはファイル名の先頭に”_”が付く。

Comment一覧へのリンクもいらないので削除。

一覧は新しくファイルを作る。

app/views/comments/_index.html.erb

<h2>Comments</h2>

<ul>
  <% if @post.comments.size < 2 %>
    <li>No comments.</li>
  <% else %>
    <% @post.comments.each do |comment| %>
      <% unless comment.id.nil? %>
        <li><%= comment.body %>
        (<%= comment.created_at %> |
        <%= link_to 'Edit', edit_post_comment_path(@post, comment) %> |
        <%= link_to 'Destroy', post_comment_path(@post, comment), confirm: 'Are you sure?', method: :delete %>)
        </li>
      <% end %>
    <% end %>
  <% end %>
</ul>

また書き込み後はComment一覧画面ではなく、そのPost参照画面に移動するようにする。

  def create
    @comment = @post.comments.build(params[:comment])

    respond_to do |format|
      if @comment.save
        format.html { redirect_to [@post, @comment], notice: 'Comment was successfully created.' }
  def create
    @comment = @post.comments.build(params[:comment])

    respond_to do |format|
      if @comment.save
        format.html { redirect_to @post, notice: 'Comment was successfully created.' }

これで一体化完了。

まあデザインはどうにかしてください。

おまけ、本文の改行を有効に

app/views/posts/show.html.erb

  <%= @post.body %>
  <%= simple_format @post.body %>

とっぴんぱらりのぷう。