views:

126

answers:

4

In my Rails app I have a fairly standard has_many relationship between two entities. A Foo has zero or more Bars; a Bar belongs to exactly one Foo. Both Foo and Bar are identified by a single integer ID value. These values are unique across all of their respective instances.

Bar is existence dependent on Foo: it makes no sense to have a Bar without a Foo.

There's two ways to RESTfully references instances of these classes. Given a Foo.id of "100" and a Bar.id of "200":

  1. Reference each Foo and Bar through their own "top-level" URL routes, like so:

    • /foo/100
    • /bar/200
  2. Reference Bar as a nested resource through its instance of Foo:

    • /foo/100
    • /foo/100/bar/200

I like the nested routes in #2 as it more closely represents the actual dependency relationship between the entities. However, it does seem to involve a lot of extra work for very little gain. Assuming that I know about a particular Bar, I don't need to be told about a particular Foo; I can derive that from the Bar itself. In fact, I probably should be validating the routed Foo everywhere I go (so that you couldn't do /foo/150/bar/200, assuming Bar 200 is not assigned to Foo 150). Ultimately, I don't see what this brings me.

So, are there any other arguments for or against these two routing schemes?

Point of Clarification

I'm concerned mostly about RESTful updates/shows/deletes to particular Bars. For getting a list of Bars for a specific Foo (which is usually the "index" action in Rails) it makes perfect sense to have a nested route such as /foo/100/bar. The page at this route could just as easily link to /bar/x as /foo/100/bar/x though.

+4  A: 

As you already know, every item has to have a unique address. I would suggest that the shorter and easier the address is, the better for everyone involved - your routing code, your client apps, etc. If you can identify a Bar by its unique ID, I would probably specify only that in the URL.

You don't need to lose the semantic information about the Bar's assignment to a Foo, though. That can be part of the representation.

As an enhancement, you could allow the client to address Bar 200 as /foo/100/bar/200, but then redirect to the preferred /bar/200/ address, using, e.g., a 303 ("See Other") response.

jwismar
That's not a bad idea. You could even go as far as to presume that the ID of the Foo is irrelevant, and thus not bother checking for it. Also: rather than a client-side redirect, you could just do a new route pointing to the same controller/action.
Craig Walker
@Craig - agreed, but if you have a preferred URI, then with a client-side redirect, you can educate the client. (In the admittedly unlikely event that the client program is keeping track of those things.)
jwismar
+5  A: 

You're looking for shallow routes. As you pointed out, the idea of having a deeply nested route for things like creates, updates is unnecessary since you are targeting the desired record directly.

I have never actually done the shallow routing thing so I'll pass on the railscast episode where Ryan Bates explains it probably better than I could: 139 Nested Resources.

Edit: You can read up a little more on the guides for routing 3.8.4.

theIV
FWIW: I deleted the "create" part, as a nested route is actually *very* useful: it lets me fill in the FK field in Bar without having to retrieve it from the posted parameters; it gets it from the route values instead.However, it still applies for updates, shows, and deletes (which I just added)
Craig Walker
As usual, Railscasts has all the answers. Shallow routes has the best of both worlds. Thanks!
Craig Walker
I'm sorry to see it's not all the way there in Rails 3, yet. But, I'm glad you figured out a solution for the time being :)
theIV
A: 

Personally, if each Bar is dependent on a Foo, option #2 makes more sense. For example, if Foo is a blog and Bar is a post, it is far more logical to access a particular post at example.bloghosting.com/blog/1/post/2 than example.bloghosting.com/post/21.

Do do this with the latest version of Rails:

class Food < ActiveRecord::Base
    has_many :bars
end

class Bar < ActiveRecord::Base
    belongs_to :foo
end

Then, in the Rails router (/config/routes.rb):

map.resources :foos do |foo|
    foo.resource :bars
end

For more information, see Rails Routing from the Outside In and the Rails API Documentation.

nickname
I agree that it's more logical from a URL point of vieew. However, what I'm asking is whether it's really necessary/beneficial. So far I don't see enough benefit to make it worth the cost.
Craig Walker
How beneficial your URL structure is to the user is dependent mainly on how easily it gives them a sense of your program's model. Hence, the closer the URL structure matches the external model of interaction for your program, the better.As for the cost... simply changing map.resources :foos to the above code is hardly a cost. If you mean usability cost, I would argue that the marginally longer URLs that better match your program's structure are superior to shorter URLs that don't.
nickname
The cost isn't just in changing the routing. You also need to change the calls to the named route methods and probably should be adding parent-object validation to the controller methods as well.
Craig Walker
It's true that there is additional cost beyond the change in routing. Have you already implemented the old routes? If so, you could use the :shallow option as a temporary measure to transfer your routes over to the new system gradually.
nickname
FWIW, Rails' shallow routes (see the accepted answer) makes the cost much less, meaning that the benefits of the logical URLs is probably worth it.
Craig Walker
+2  A: 

Rails' shallow nesting gives us the best of both worlds: it lets us have Foo-specific routes where it makes sense (create, new, index) and Bar-only routes otherwise (show, edit, update, destroy).

Unfortunately, the new routing DSL in Rails 3 (which I'm using) does not support shallow routing yet (as of Beta 3). It appears to be in the works but it's not functional for today.

However, you can fake it by mapping the resource twice, once nested and once shallow:

resources :foo do
  resources :bar, :only => [:index, :new, :create]    
end
resources :bar, :only => [:show, :edit, :update, :destroy]

This will tide me over until the :shallow option is up and running again.

Craig Walker