When I first heard about nested routes, as with most things in Rails, I completely ignored them, discounting them as something I’d get to learning when I needed to, but right now they were yet another one of those things that I didn’t quite grok as I didn’t have the other base knowledge of the Rails system to know why I needed them. To be fair, I’m not that much closer now, and started to realize the the other day I didn’t really quite understand REST and having a RESTful site.
But when I started looking at how my site was structured, and how things were really encapsulated in each other, such as clubs “having” fields, and fields “having” games, I started to understand the need. Actually it was more doing the scaffolding of the site and seeing that I had to set field in a dropdown each time I created a game, or the club each time I created a field. It didn’t make sense and it seemed like a stupid way to do it.
Back in the old days in perl, I’d pass the ID of the main object when I created a sub-object. So if I had a club editing page with a ‘create new field’ button on it, that button would pass a hidden “club_id=$id” when it was pressed.
Rails comes with a nicer way of doing it. It allows you to nest related resources. Probably the best place to start is with Ryan Bates excellent Nested Resources screencast, or Adam’s Nested Resources in Rails 2 page.
For this I set up the following in my routes.rb as such:
map.resources :clubs, :has_many => :areas, :shallow => true
And then running “rake routes” will show you what the routes will actually be:
club_areas GET /clubs/:club_id/areas(.:format) {:action=>"index", :controller=>"areas"}
POST /clubs/:club_id/areas(.:format) {:action=>"create", :controller=>"areas"}
new_club_area GET /clubs/:club_id/areas/new(.:format) {:action=>"new", :controller=>"areas"}
clubs GET /clubs(.:format) {:action=>"index", :controller=>"clubs"}
POST /clubs(.:format) {:action=>"create", :controller=>"clubs"}
new_club GET /clubs/new(.:format) {:action=>"new", :controller=>"clubs"}
edit_club GET /clubs/:id/edit(.:format) {:action=>"edit", :controller=>"clubs"}
club GET /clubs/:id(.:format) {:action=>"show", :controller=>"clubs"}
PUT /clubs/:id(.:format) {:action=>"update", :controller=>"clubs"}
DELETE /clubs/:id(.:format) {:action=>"destroy", :controller=>"clubs"}
The biggest, biggest thing to notice here is the “:id” and “:club_id” in some of the routes. This tripped me up a few times.
Nested routes let you automagically pass “club_id” (in this case) to paths. So in your controller or views you can now use the variable from the first column. So you can do a
redirect_to clubs_path
# instead of:
redirect_to :controller => "clubs", :action => "index"
for example, and it would go to the /clubs/index path and do the right thing. It also lets you do things like:
redirect_to new_club_area(@club)
# sends you to /clubs/1/areas/new
Note that you have to pass the club before this will work. This took me forever to figure out, cause sometimes it worked and sometimes it didn’t. What I found was looking for either :id or :[object]_id in the routes path would tell you both if you have to pass the route the object or not, but also if you’re processing the path in the controller, that :club_id will be passed auto-magically to it. So for the “/clubs/:club_id/areas/new” route above, you’d process it in your areas_controller, and in the ‘new’ def, you’d automatically get club_id for free. IE:
# in app/controllers/area_controller.rb, possibly via new_club_area_path(@club)
def new
@club = Club.find(params[:club_id])
@area = @club.areas.new
end
You know that club_id is being passed because the rake_route for that path has “club_id” in it: “/clubs/:club_id/areas/new”.
Next up, finishing converting as much of the rest of my code from the controller/action syntax for redirecting and rendering to a more “restful” setup.
Update: There is a cool sounding TextMate bundle to help you with some of the brain games needed to understand nested resources. Hat tip to The Ruby Show.