Occasional encounters with Ruby on Rails.

Thursday, February 22, 2007

Nested Polymorphic Active Resources (Revisited)


Quick update on my previous post regarding Nested Polymorphic Active Resources. I found that in some cases it was really handy to have the find_polymorphic_resource method in actions other than index and create. When I tried making the call, however, it didn't work. It turns out that the logic at the end of the find_polymorphic_type_and_id of the PolymorphicResource module was flawed.

Originally the method closed with:

sections.pop

That works great for nested resources that need to know their owner for index and create because there is only one element in the sections array. It does not work at all if you're using one of the other RESTful methods like show or edit. In those cases both the owner and the child show up on the url as

GET /owner/:owner_id/child/:child_id

To work around this I've simply changed the last line of the find_polymorphic_type_and_id method to read as follows:

sections.first

The logic behind this is simple. When you have only one element in the sections array (index/create) then you return the one object -- that's the parent and the same thing returned by sections.pop. When you have a nested path (show/edit) it's the first element in the array that has the parent.

This clearly has its limitations, but they are the same limitations as those imposed by the original solution -- it only supports one level of nesting. If you wanted to support multiple levels of nesting you could probably just index the sections array more explicity. For example, the immediate parent could be found at

sections[sections.length-2]

Okay, I didn't realize that last bit was quite so simple. I was lazy and went for the quick way out. Time to go up date my module...

Friday, February 02, 2007

Nested Polymorphic Active Resources



As has been mentioned elsewhere, it'd be awfully nice for Rails' ActiveResources to deal with Polymorphic nested relationships. Huh? Say you wanted to do something like acts_as_commentable, which allows you to add comments to any object through polymorphism. How do you make that RESTful, so that you could have a nice URL like blog/123/comment/127 and another like books/987/comment/29?

To start, you've obviously got to nest the resources. For the examples urls above you would add the following to your routes.rb:


map.resources :blogs do |blog|
blog.resources :comments, :name_prefix => 'blog_'
end
map.resources :books do |book|
book.resources :comments, :name_prefix => 'book_'
end


The :name_prefix option serves two purposes. First, it makes sure that the routes don't collide. Without it the second mapping would override the first and you'd never get to the blog comments. Second, it makes it possible to have clearly named routes. If you want to generate the url for the book comments you'll use book_comments_path().

Okay, that makes creating the routes easy enough, but what about processing them from the controller. How does the Comments controller, for example, know whether to find a Book object or a Blog object when it wants to list comments? And how does it figure out which Book or Blog object to instantiate? There was a good conversation about this on RailsWeenie. The solution -- determining the 'parent' object from the mappings -- was good but but could be run through the DRYer because, chances are, if you're using polymorphism then you're using it in more than one place.

To that end I created a module so that I did not need to recreate the code every time I wanted to get polymorphic. It's not much, but it's effective.


module PolymorphicResource
def self.included(base)
base.class_eval do
private
def find_polymorphic_resource
poly_type, poly_id = find_polymorphic_type_and_id
eval("%s.find(poly_id)" % poly_type)
end

def find_polymorphic_type_and_id
# just finds instances of /controller/id
sections = request.env['REQUEST_URI'].scan(%r{/(\w+)/(\d*)})

sections.map! do |controller_name, id|
[controller_name.singularize.camelize, id]
end

sections.pop # support one-level of polymorphism
end
end
end
end


With that in place, we've got to do a little work in the controller. First, we've got to extend the controller by adding the module so we add 'require PolymorphicResource' to the top of the controller class definition. Now my controller can now find the 'parent' with a simple call to find_polymorphic_resource. For example, the comments_controller#index method could be modified like this:

def index
@commentable = find_polymorphic_resource
@comments = @commentable.comments.find(:all)
...
end


And the comments_controller#create would be

def create
@commentable = find_polymorphic_resource
@comment = @commentable.comments.build(params[:comment])
...
end


The RailsWeenie conversation mentioned above makes a good point that the only places in which you need to add the call to find_polymorphic_resource are in index and create. Everywhere else you can find the polymorphic entity through a reference to the referenced object. For example, the comments_controller#destroy method would now look like this:

def destroy
@comment = Comment.find(params[:id])
@comment.destroy

respond_to do |format|
format.html { redirect_to @comment.commentable.class.is_a?(Book) ? book_comments_url(@comment.commentable) : blog_comments_url(@comment.commentable)}
format.xml { head :ok }
end
end