Nested Resources in Rails 2
Posted by Adam Wiggins on December 20, 2007 at 02:44 AM
Nested resources were introduced in Rails 1.2 and are touted as the Right Way to do REST with parent-child model associations. If your app has a url that reads something like /employees?company_id=1, a switch to nested resources would cause it to read /companies/1/employees.
Rails 2 introduced a few subtle but important syntax changes. So far I haven't seen any comprehensive guide to the new syntax, so I'm writing one.
I'll use the example that seems to be popular, which is tickets belonging to events:
class Event < ActiveRecord::Base has_many :tickets end class Ticket < ActiveRecord::Base belongs_to :event end
Full code for the example app is available for browsing or download via svn. Most of the relevant code is in routes.rb, tickets_controller.rb, and the tickets views.
Routes
The first step (and the easiest one) is setting up the named route with :has_many syntax. (Note that this is a significant change from Rails 1.2 syntax of passing a block to map.resources.)
map.resources :events, :has_many => :tickets
Should there be a separate line reading map.resources :tickets? That depends on whether you want the tickets to be accessible in a non-nested form (e.g. //1). Given that a ticket will always have an event, I think it's more consistent not map tickets separately. The nesting information from the url isn't needed for some operations (show, edit, and destroy). But then you'd need to have fine-grained exceptions to the before_filter, deciding when to pull the event from the url and when not to. I don't think there's any consensus on this point yet, but for this post I'm going to use the always-nested approach.
Route Helpers
The next part I've found to be the hardest - or at least, rather time-consuming. It's somewhat difficult to remember the right syntax, since there are a dozen or so helpers to generate all the urls that are needed. Thankfully there's a rake task to show you all the named routes: rake routes. The output looks like this:
events GET /events {:controller=>"events", :action=>"index"} formatted_events GET /events.:format {:controller=>"events", :action=>"index"} POST /events {:controller=>"events", :action=>"create"} POST /events.:format {:controller=>"events", :action=>"create"} new_event GET /events/new {:controller=>"events", :action=>"new"} formatted_new_event GET /events/new.:format {:controller=>"events", :action=>"new"} ...
Blood started squirted out of my eyes the first time I ran this task on an app with nested resources - it's not pretty if your terminal isn't wide enough to prevent the lines from wrapping. I suggest maximizing your window to prevent getting blood all over your keyboard.
The bit we're looking for is in the far lefthand column - the name of the route. For our nested resource, here's the interesting ones:
event_tickets GET /events/:event_id/tickets new_event_ticket GET /events/:event_id/tickets/new edit_event_ticket GET /events/:event_id/tickets/:id/edit event_ticket GET /events/:event_id/tickets/:id
The naming scheme is: parent resource (singular), then child resource (plural). So where you might have used tickets_path before, you now use event_tickets_path. new_ticket_path becomes new_event_ticket, and so on.
Seem simple? Not so fast. You also need to include the event as a parameter. So tickets_path becomes event_tickets_path(event). In cases where the child resource already knows its parent, such as an edit link, you can use edit_event_ticket_path(ticket.event, ticket). (This last bit wouldn't be necessary if you chose to map.resource :tickets, in which case editticketpath(ticket) would still work. The downside is that then you have to remember when you need to use nested helpers and when you don't. As mentioned above, I prefer going the route of consistency - always nested.)
Forms
What else needs to change? form_for has this syntax with resources:
<% form_for(@event) do |f| %>
Which is wonderfully succinct compared to the way that non-resource form_fors usually look. But it gets a little funky with nested resources:
<% form_for([ @event, @ticket ]) do |f| %>
(Trevor Squires makes a good argument to why this syntax isn't too spiffy, and Codafoo makes a slightly less compelling argument as to why it is.)
Redirects
Redirects and XML locations should also use the form_for argument syntax:
if @ticket.save flash[:notice] = 'Ticket was successfully created.' format.html { redirect_to([ @event, @ticket ]) } format.xml { render :xml => @ticket, :status => :created, :location => [ @event, @ticket ] }
In all cases, the parent resource always goes first - same as the url helper, i.e. event_ticket_path.
Before Filter
Since the controller is never called without nesting, the before_filter is simple:
before_filter :get_event def get_event @event = Event.find(params[:event_id]) end
Thus, every action and view can always count on @event being set. In some cases you can access @ticket.event, but in the always-nested approach, @event can be used everywhere.
Scoping
Any place the controller makes an ActiveRecord call like find or new should be scoped:
def index @tickets = @event.tickets.find(:all) def show @ticket = @event.tickets.find(params[:id]) def new @ticket = @event.tickets.new
One trick for making sure you've changed every reference is to search the file for the class name. The text "Ticket" should not appear in tickets_controller.rb, except on the first line as part of the controller name.
Conclusion
Getting this all set up is quite a bit of busywork if you start with two models generated with resource scaffolding. (Which reminds me: scaffold_resource from Rails 1.2 is gone, replaced by scaffold in Rails 2. The original scaffold is gone, which is good, because last I checked it had suffered some serious bitrot.)
It would be immensely convenient if there were a generator for this. Something like:
generate nested_resource Ticket belongs_to:event
However, this would be quite a bit more difficult to write than a typical generator, because it would need to modify existing code beyond just adding lines to a file. So although handy, don't count on seeing this anytime soon. Though if some enterprising soul wanted to put their mind to it, I'm sure the Rails community would be forever, or at least briefly, grateful.
Comments
There are 10 comments on this post. Post yours →
Heya, thanks for this, it makes it very clear.
Especially if you are doing RSpec on views and controllers and trying to get your head around what the views and controllers are meant to return... before you can see them.
Thanks.
Mikel
Great tutorial!!
Kudos! This is exactly what I needed.
Nice post, it helped give me a name to what I was trying to do... nested resources.
I'm actually trying to take this concept and apply it to the ASP.NET MVC framework and I've been looking at a lot of Rails code trying to figure out how everyone else tackles this issue. Do people nest resources even three deep? I'm struggling with something like this:
A course has many sections and a section has many classes. So I need to end up with a url like this:
/courses/1/sections/2/classes
Which is about making my head split open with all the permutations. At least it was nice to see that /courses/1/sections is the approved way to go instead of /sections?courseid=1, which felt sorta dirty.
Wonderfully helpful post. Thanks!
Good post Adam. Pretty helpful. Shawn, a good rule of thumb is to not have more than one level of nesting resources. See what Jamis Buck has to say here: http://weblog.jamisbuck.org/2007/2/5/nesting-resources
Can you use this technique with something other than the "id"? I'd like to tokenize the ID so it was non-guessable.
Excellent! Setting up the named routes is much easier than in previous Rails versions.
Thanks,
Patrick
Thanks! This took some guess work out of my work. Interesting idea about writing a generator. Maybe I'll get to that someday.
Hi,
class Book < ActiveRecord::Base belongs_to :author end
class Author < ActiveRecord::Base has_many :books end
And I am using
before_filter :abc def abc @author = Author.find(params[:author_id]) end
I am getting an error "Couldn't find Author without an ID"
How do I correct this as I have no idea about this
Post a comment
Required fields in bold.