Nested Resources in Rails 2

rails

Thu Dec 20 02:44:00 -0800 2007

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. 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 edit_ticket_path(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.