Introduction

In REST-style architecture , a resource is simply a source of information, the one you want to expose. The resource is referenced thanks to a global identifier such as an URI.

For example, in our own PullReview, Code Reviews and Users are resources with their related routes and actions.

When you deal with information structure such as composition, you'll use nested resources. The reference of the embedded resource is then built over the reference of the composite resource.

Ruby on Rails allows you to set up nested resources. For instance in the "Getting started" guide where you build a very simple blog, Post and Comment are nested resources. Indeed, it is impossible to consider a lone comment without any post. A Comment belongs to a Post. Resources being referenced by URIs, the setup of nested resources in RoR is done through routing as following:

resources :posts do
 resources :comments
end

But in the example of the guide, a Comment hasn't got its own view. A Comment is managed through the views of its Post. It's totaly suited to a blog, but in another case, maybe you would like to program nested resources with independant views, i.e. each resource has their views. This is the goal of this tutorial. I start from the same blog example, but this time I'll generate scaffolds for both models, Post and Comment.

Same first steps: Post model

First, create the blog application:

rails new blog
cd blog

Generate a scaffolded Post resource:

rails generate scaffold Post name:string title:string content:text

Add some validation to it (app/models/post.rb):

class Post < ActiveRecord::Base
 validates :name,  :presence => true
 validates :title, :presence => true, :length => { :minimum => 5 }
end

Until now, it's totally the same than in the guide. The next step starts the fork!

Fork: Comment Model

Generate a scaffolded Comment resource:

rails generate scaffold Comment commenter:string body:text post:references

Edit the app/models/post.rb file to add the other side of the association:

class Post < ActiveRecord::Base
  validates :name,  :presence => true
  validates :title, :presence => true, :length => { :minimum => 5 }

  has_many :comments
end

Setup nested resources (config/routes.rb):

resources :posts do
  resources :comments
end

Add some validations to the Comment resource (app/models/comment.rb):

class Comment < ActiveRecord::Base
  validates :commenter, :presence => true
  validates :body, :presence => true

  belongs_to :post
end

Comment controller

Edit the Comment controller app/controllers/comments_controller.rb:

class CommentsController < ApplicationController
  # GET /posts/:post_id/comments
  # GET /posts/:post_id/comments.xml
  def index
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you get all the comments of this post
    @comments = post.comments

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

  # GET /posts/:post_id/comments/:id
  # GET /comments/:id.xml
  def show
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])

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

  # GET /posts/:post_id/comments/new
  # GET /posts/:post_id/comments/new.xml
  def new
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you build a new one
    @comment = post.comments.build

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

  # GET /posts/:post_id/comments/:id/edit
  def edit
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])
  end

  # POST /posts/:post_id/comments
  # POST /posts/:post_id/comments.xml
  def create
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you create the comment with arguments in params[:comment]
    @comment = post.comments.create(params[:comment])

    respond_to do |format|
      if @comment.save
        #1st argument of redirect_to is an array, in order to build the correct route to the nested resource comment
        format.html { redirect_to([@comment.post, @comment], :notice => 'Comment was successfully created.') }
        #the key :location is associated to an array in order to build the correct route to the nested resource comment
        format.xml  { render :xml => @comment, :status => :created, :location => [@comment.post, @comment] }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /posts/:post_id/comments/:id
  # PUT /posts/:post_id/comments/:id.xml
  def update
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])

    respond_to do |format|
      if @comment.update_attributes(params[:comment])
        #1st argument of redirect_to is an array, in order to build the correct route to the nested resource comment
        format.html { redirect_to([@comment.post, @comment], :notice => 'Comment was successfully updated.') }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/:post_id/comments/1
  # DELETE /posts/:post_id/comments/1.xml
  def destroy
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])
    @comment.destroy

    respond_to do |format|
      #1st argument reference the path /posts/:post_id/comments/
      format.html { redirect_to(post_comments_url) }
      format.xml  { head :ok }
    end
  end
end

The important changes are the following:

  • Retrieve a Comment
    post = Post.find(params[:post_id])
        @comment = post.comments.find(params[:id])
  • Retrieve all Comments
    post = Post.find(params[:post_id])
        @comments = post.comments
  • Building of a new Comment
    post = Post.find(params[:post_id])
        @comment = post.comments.build
  • Creation of a new Comment
    post = Post.find(params[:post_id])
        @comment = post.comments.create(params[:comment])
  • Redirection to the Comment resource
    redirect_to([@comment.post, @comment], :notice => 'Comment was successfully created.')
  • Redirection to the list of Comments
    redirect_to(post_comments_url)

Comment Views

Edit the views app/views/comments/:

  • _form.html.erb: remove the :post member
    <%= form_for([@comment.post, @comment]) do |f| %>
      <% if @comment.errors.any? %>
        <div id="error_explanation">
          <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
    
          <ul>
          <% @comment.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
          <% end %>
          </ul>
        </div>
      <% end %>
    
      <div class="field">
        <%= f.label :commenter %><br />
        <%= f.text_field :commenter %>
      </div>
      <div class="field">
        <%= f.label :body %><br />
        <%= f.text_area :body %>
      </div>
      <div class="actions">
        <%= f.submit %>
      </div>
    <% end %>
  • edit.html.erb: update link
    <h1>Editing comment</h1>
    
    <%= render 'form' %>
    
    <!-- /posts/:post_id/comments/:id -->
    <%= link_to 'Show', [@comment.post, @comment] %> |
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post) %>
  • index.html.erb: remove :post member and update link
    <h1>Listing comments</h1>
    
    <table>
      <tr>
        <th>Commenter</th>
        <th>Body</th>
        <th>Post</th>
        <th></th>
        <th></th>
        <th></th>
      </tr>
    
    <% @comments.each do |comment| %>
      <tr>
        <td><%= comment.commenter %></td>
        <td><%= comment.body %></td>
    
        <!-- /posts/:post_id/comments/:id -->
        <td><%= link_to 'Show', [comment.post, comment] %></td>
        <!-- /posts/:post_id/comments/:id/edit -->
        <td><%= link_to 'Edit', edit_post_comment_path(comment.post, comment)%></td>
        <!-- /posts/:post_id/comments/:id -->
        <td><%= link_to 'Destroy', [comment.post, comment], :confirm => 'Are you sure?', :method => :delete %></td>
      </tr>
    <% end %>
    </table>
    
    <br />
    
    <%= link_to 'New Comment', new_post_comment_path %> <!-- /posts/:post_id/comments/new -->
  • new.html.erb: update link
    <h1>New comment</h1>
    
    <%= render 'form' %>
    
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post)%>
  • show.html.erb: remove :post member and update link
    <p id="notice"><%= notice %></p>
    
    <p>
      <b>Commenter:</b>
      <%= @comment.commenter %>
    </p>
    
    <p>
      <b>Body:</b>
      <%= @comment.body %>
    </p>
    
    <!-- /posts/:post_id/comments/:id/edit -->
    <%= link_to 'Edit', edit_post_comment_path(@comment.post, @comment) %> |
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post) %>

And finally, add a link to the show view of post app/views/posts/show.html.erb:

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

<p>
  <b>Name:</b>
  <%= @post.name %>
</p>

<p>
  <b>Title:</b>
  <%= @post.title %>
</p>

<p>
  <b>Content:</b>
  <%= @post.content %>
</p>

<!-- /posts/:post_id/comments/ -->
<%= link_to 'Comments', post_comments_path(@post) %> |
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>

As summary, the following table contains the paths used for the Comment resource:
[table id=1 /]

Final commands

Check the routes:

rake routes
    post_comments GET    /posts/:post_id/comments(.:format)          {:controller=>"comments", :action=>"index"}
                  POST   /posts/:post_id/comments(.:format)          {:controller=>"comments", :action=>"create"}
 new_post_comment GET    /posts/:post_id/comments/new(.:format)      {:controller=>"comments", :action=>"new"}
edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format) {:controller=>"comments", :action=>"edit"}
     post_comment GET    /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"show"}
                  PUT    /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"update"}
                  DELETE /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"destroy"}
            posts GET    /posts(.:format)                            {:controller=>"posts", :action=>"index"}
                  POST   /posts(.:format)                            {:controller=>"posts", :action=>"create"}
         new_post GET    /posts/new(.:format)                        {:controller=>"posts", :action=>"new"}
        edit_post GET    /posts/:id/edit(.:format)                   {:controller=>"posts", :action=>"edit"}
             post GET    /posts/:id(.:format)                        {:controller=>"posts", :action=>"show"}
                  PUT    /posts/:id(.:format)                        {:controller=>"posts", :action=>"update"}
                  DELETE /posts/:id(.:format)                        {:controller=>"posts", :action=>"destroy"}

Create and migrate the db:

rake db:create
rake db:migrate

Run the server:

rails server

Voilà!