Symmetrical Routes for Nested Resources

Subscribe to Symmetrical Routes for Nested Resources 4 posts, 2 voices

 
Avatar SingleShot 2 posts

I’m having trouble setting up nested resources in a particular way that makes sense to me.

Given rules like these:
  • an owner owns many books
  • a book can be owned by many owners
  • an owner can own multiple copies of the same book

I have a many-to-many relationship of Owners-to-Books with the “link” table (we’ll call it “Ownerships” since I can’t think of a nice name) having an attribute for tracking the number of copies of a book an owner owns.

I have no problem listing all books, or listing all owners: What I can’t figure out is how to set up my routes/controllers to list all books owned by a particular owner, and all owners that own a particular book (i.e. what I call “symmetrical routes for nested resources”). I’d like the URLs to look something like this:

What’s extra tricky is that I want the listings to also contain the number of copies from the “link” table. So, for example, http://127.0.0.1:3000/owners/7/books.xml might return the equivalent of this as XML:

To Kill A MockingBird, 2 The Zombie Survival Guide, 1 War and Peace, 4

Please help this noob :-)

Thanks,

Mike

 
Avatar Jeff Cohen 89 posts

Hey Mike,

Be forewarned I haven’t tried any of the following to check my syntax, and I’m assuming you’re on Rails 2.0 or higher….

I assume you’ve already done something like this to get the “normal” routes working:

map.resources :books
map.resources :owners

But it may not be obvious that you can do this:

map.resources :books, :has_many => :owners
map.resources :owners, :has_many => :books

This means that the OwnersController’s index action will be called for both /owners.xml and also for books/3/owners.xml. In the first case, you want all owners; in the second, you’re being asked only for owners of book 3.

So a common approach is to use a before_filter to find out which scenario we’re in. Also, it sounds like once you find the owners, you need to transform those to the ownerships:

class OwnersController < ApplicationController

before_filter :find_book

def index
 # If we're being used as an "inner" resource, then
 # only get the owners for the given book
 @owners = @book.owners if @book

 # Otherwise, we should get all books
 @owners ||= Owner.find(:all)  # Or you can do Owner.all in Rails 2.1 or above)

 respond_to do |format|
   format.html  # render html template
   format.xml   # render xml template
 end

end

def find_book
 # Find the "outer" resource if necessary
 @book = Book.find(params[:book_id]) if params[:book_id]
end

In your XML template, you can loop through all the ownerships, and whenever you need the number of copies, use the ownership data:

 xml.book_data do 

   @owners.each do |owner|
      xml.title owner.ownership.book.title
      xml.copies owner.ownership.copies
   end

end

Does this help at all?

 
Avatar SingleShot 2 posts

Wow. I can’t imagine a better response to a software development question. It’s precisely what I needed, and it worked like a charm. Thanks!

 
Avatar Jeff Cohen 89 posts

Great!