Nested CRUD resources in Rails 1.2
Jan 10 by
Andre
The CRUD methodology in Rails 1.2 is a great way to simplify your application structure. One of the first things you will find yourself doing is nesting one resource inside another. This is just the RESTful way of working with has_many relationships. The first couple times I did this, I forgot some of the steps (particularly changing the url paths in the controllers). So, I'm going to iterate the steps here -- hopefully this will help someone going through the learning process on Rails 1.2 CRUD.
1. Create the resource scaffolding and migrations
Don't worry, scaffold_resource doesn't have the same stigma that old-school scaffolds had back in the day. Scaffold_resource is the easiest way to jump-start a RESTful controller, and the actions/views it creates are both legitimate and useful. Let's step through the process with post has_many :commentsruby script/generate scaffold_resource Post title:string body:text
ruby script/generate scaffold_resource Comment body:text
FYI, I like to create my migrations first, and just use scaffold_resource to generate whatever subset of columns I'd like to get a jumpstart with in the UI. Your preference may vary.
Before running your migrations, go into the comments migration and add the foreign key to posts: t.column :post_id, :integer.
2. Set up relationships in models.
Nothing unusual here:
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
3. Modify routes.rb
The scoffold_resource generator calls will have created two lines in routes.rb. You just have to nest them like so:map.resources :posts do |posts|
posts.resources :comments
end
. . . which basically just says that comments will be accessed in the context of a particular post.
4. Modify the controller of the nested resource
The controller for the nested resource (comments) needs a few changes to account for the fact that comments will always be accessed in the context of a post.a) add a before_filter:
before_filter(:get_post)
. . . .
private
def get_post
@post = Post.find(params[:post_id])
end
b) ammend the *_url calls in create and update actions
Look in the create and update actions for the comment_url calls. The call needs the nestee resource to properly generate the URL. In our case, it will look like:From: format.html { redirect_to comment_url(@comment) }
To: format.html { redirect_to comment_url(@post,@comment) }
You'll need to make changes in both the update and create actions.
c) scope your Comment (nested class) finders to the Post (nestee class)
This is a security measure -- it ensures that the Comment ID passed to the controller really does represent a comment which belongs to the Post in question. It's all part of ensuring that the nested class is always dealt with in the context of a nestee class. In terms of your controller code, this means finding instances of comment.find and changing them to @post.comments.find. Don't forget the find :all in the index action!5. Modify the views
The only thing you need to change is a couple of *_path calls -- the same kind of updates you made in the controller. In each case you need to add the nestee class in *before* the nested class, so the method can properly generate the URL. For example, in the show.rhtml template, the link at the bottom is changed like so:From: <%= link_to 'Show', comment_path(@comment) %>
To: <%= link_to 'Show', comment_path(@post, @comment) %>
You need to make changes in three files for the nested resource (comments in our case):
- edit.rhtml: the url in form_for, and the link_to 'show' at the bottom
- show.rhtml: the link_to 'edit' at the bottom
- index.rhtml: the link_to 'edit' and the link_to 'destroy'
6. Provide a link to the nested resource
Finally, let's add a link in views/posts/index.rhtml, so we can easily navigate to the comments associated with a posts: <%= link_to 'see comments', comments_path(post)%>You're done!
That's all you need to do to get your CRUD scaffolding off the ground with nested resources. With the basics of a nested resource in place, you can now refactor the scaffolding to build the interface you need to. For example, you might want to get rid of the views/comments/index.rhtml, and list the comments on the post page instead. Enjoy the CRUD!*Bonus Round*: DRY up finders in the Comments Controller
Notice that there are similar finders in (@post.comments.find(params[:id])) in your Comment's show, edit, update, and delete actions. You can DRY it up by removing those and adding the finder in your before_filter:before_filter(:get_post)
. . . .
private
def get_post
@post = Post.find(params[:post_id])
@comment = @post.comments.find(params[:id]) if params[:id]
end

Comments
Tammer Saleh on Jan 10
Good article. It's also worth a mention that you should be using the model associations instead of the direct finders in your comments controller:
..and so on.
Erik Kastner on Jan 16
Awesome stuff! I'll mention it down here since it's possibly beyond the scope of the post, but after you have the before_filter, you need to update your tests for the nested resource, they'll fail without a :post_id => on the requests.
Andre on Jan 16
Tammer: Thanks for the reminder, I integrated this into the example, together with a "bonus round" for DRYing up the finders.
Erik: good point re: testing the nested resources
Danno on Jan 22
Thanks Andre, this information is in the Second Edition of the Rails book but it's a bit scattered.
You also added some good tips!
Per Velschow on Jan 22
Interesting article! One thing has to strike you though. If this is what you will be doing most of the time when dealing with nested models, doesn't it seem like Rails should offer more direct support for this? Though the changes do not seem hard to make, manual, repetitive changes are always prone to errors.
So I was thinking something along the lines of adding has_many/belongs_to methods to the controllers similar to how it works for the models. The controller could even derive this relationship directly from the models. I haven't thought it through entirely. But one thing I imagine it would mean is that all find methods inside the comments controller would implicitly be scoped with a specific post. The named comment routes would also be auto-generated. Or they might even be implicit too; i.e. comment_path would always be in reference to the current post.
Just some vague ideas. :)