Nested Resources

written by stonean on June 6th, 2008 @ 11:16 AM

Nested resources sound nice and look cool on the url:

http://wickedcoolblog.com/posts/1/comments

In order for this to work, you have to define this relationship in your routes file as such:

  map.resources :posts do |posts|
    posts.resources :comments
  end

So what does this mean from a content structure? Does the comments controller reside in a posts directory? Is the comments controller namespaced with posts?

Fortunately neither of these constructs are required. Quite simply, when you call the above url you are getting the following parameters:

{"action"=>"index", "post_id"=>"1", "controller"=>"comments"} 

As you can see, you're still just accessing the comments controller and the only thing special about it is the post_id parameter. Your comments controller needs to process this parameter to ensure that all comments belong to the post_id given.

In the resource oriented construct of a REST (Representational Stage Transfer) design this doesn't quite match. With the above nested route definition, you won't be able to access the comments resource without going through the posts resource. This can be fixed with a slight addition to your routes file:

  map.resources :posts do |posts|
    posts.resources :comments
  end

  map.resources :comments

Notice that comments are defined twice. This now gives you the ability to access:

http://wickedcoolblog.com/posts/1/comments

and

http://wickedcoolblog.com/comments

Now we have an issue with our comments controller. If we try to access "/comments", we don't get the post_id passed but our previous code (with the nested resource) depended on it.

If you had designed your initial controller with a before filter, this change would be a lot easier. For example:

class CommentsController < ApplicationController
	before_filter :find_models, :only => :index
	before_filter :find_model_instance, 
                           :only => [:show, :edit, :update, :destroy]
...
  private
    def find_models
      @comments = Comment.find(:all, 
                        :conditions => ["comments.post_id = ?", params[:post_id]])
    end

    def find_model_instance
      @comment = Comment.find(params[:id], 
                       :conditions => ["comments.post_id = ?", params[:post_id]])
    end
end

To add the ability to access the comments resource without going through products, you need to modify this a bit:

class CommentsController < ApplicationController
	before_filter :find_models, :only => :index
	before_filter :find_model_instance, 
                           :only => [:show, :edit, :update, :destroy]
...
  private
    def find_models
      if params[:post_id]
        @comments = Comment.find(:all, 
                        :conditions => ["comments.post_id = ?", params[:post_id]])
      else
        @comments = Comment.find(:all)
      end
    end

    def find_model_instance
      if params[:post_id]
        @comment = Comment.find(params[:id], 
                       :conditions => ["comments.post_id = ?", params[:post_id]])
      else
        @comment = Comment.find(params[:id])
      end   
    end
end

**Note: This could be refactored to be more "DRY" and should have some security code added, but will serve for demonstration purposes.

There has been quite a lot of discussion on nested resources and issues with people not understanding when to use them. So, coming up soon is an article focused on what to look for when making a decision to use nested resources.

Nothing should be used all the time. In the case of nested resources, there only a few times they should be used and never more than one level deep.

Comments are closed