Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does anybody have any tips for managing polymorphic nested resources in Rails 3?

In config/routes.rb:

resources :posts do
  resources :comments
end

resources :pictures do
  resources :comments
end

I would like to allow for more things to be commented on as well.

I'm currently using mongoid (mongomapper isn't as compatible with Rails 3 yet as I would like), and comments are an embedded resource (mongoid can't yet handle polymorphic relational resources), which means that I do need the parent resource in order to find the comment.

Are there any elegant ways to handle some of the following problems:

In my controller, I need to find the parent before finding the comment:

if params[:post_id]
  parent = Post.find(params[:post_id]
else if params[:picture_id]
  parent = Picture.find(params[:picture_id]
end

which is going to get messy if I start adding more things to be commentable.

Also url_for([comment.parent, comment]) doesn't work, so I'm going to have to define something in my Comment model, but I think I'm also going to need to define an index route in the Comment model as well as potentially an edit and new route definition.

There might be more issues that I have to deal with as I get further.

I can't imagine I'm the first person to try and solve this problem, are there any solutions out there to make this more manageable?

like image 732
Ryan Avatar asked Sep 10 '10 00:09

Ryan


People also ask

Should you add more models to your Rails application?

As you add complexity to your Rails applications, you will likely work with multiple models, which represent your application’s business logic and interface with your database.

What can you do with a Rails application?

With your Rails application in place, you can now work on things like styling and developing other front-end components. If you would like to learn more about routing and nested resources, the Rails documentation is a great place to start.

Why Ruby on Rails is the best choice for web development?

One of the reasons why Ruby on Rails is considered a strong contender in the world of web development and is largely favored by startups worldwide is because it is highly time-efficient. One of the aspects that makes Ruby easy to use is its super-simple routing setup.

What are the different types of routing paradigms in Ruby?

This comes in handy when you’re trying to define a route that is different from the seven routes defined by the RESTful routing by default. An example of such a route can be a preview or search feature. Ruby offers two different kinds of routing paradigms to handle such routes and their context – model routes and collection routes.


2 Answers

I had to do something similar in an app of mine. I took what I came up with and changed it around a bit, but I haven't tested it, so use with care. It's not pretty, but it's better than anything else I was able to think of.

In routes.rb:

resources :posts, :pictures

controller :comments do
  get '*path/edit' => :edit, :as => :edit_comment
  get '*path'      => :show, :as => :comment
  # etc. The order of these is important. If #show came first, it would direct /edit to #show and simply tack on '/edit' to the path param.
end

In comment.rb:

embedded_in :commentable, :inverse_of => :comments

def to_param
  [commentable.class.to_s.downcase.pluralize, commentable.id, 'comments', id].join '/'
end

In a before filter in comments_controller.rb:

parent_type, parent_id, scrap, id = params[:path].split '/'

# Security: Make sure people can't just pass in whatever models they feel like
raise "Uh-oh!" unless %w(posts pictures).include? parent_type

@parent = parent_type.singularize.capitalize.constantize.find(parent_id)
@comment = @parent.comments.find(id)

Ok, ugliness over. Now you can add comments to whatever models you want, and simply do:

edit_comment_path @comment
url_for @comment
redirect_to @comment

And so on.

Edit: I didn't implement any other paths in my own app, because all I needed was edit and update, but I'd imagine they'd look something like:

controller :comments do
  get    '*path/edit' => :edit, :as => :edit_comment
  get    '*path'      => :show, :as => :comment
  put    '*path'      => :update
  delete '*path'      => :destroy
end

The other actions will be trickier. You'll probably need to do something like:

  get  ':parent_type/:parent_id/comments'     => :index, :as => :comments
  post ':parent_type/:parent_id/comments'     => :create
  get  ':parent_type/:parent_id/comments/new' => :new,   :as => :new_comment

You'd then access the parent model in the controller using params[:parent_type] and params[:parent_id]. You'd also need to pass the proper parameters to the url helpers:

comments_path('pictures', 7)
like image 107
PreciousBodilyFluids Avatar answered Jan 02 '23 12:01

PreciousBodilyFluids


Ryan Bates covered polymorphic associations in Railscasts #154, but the example was for Rails 2 and Active Record. I managed to get his example working using Rails 3 and Mongoid by making a few changes.

In the Post and Picture models, add the following line:

embeds_many :comments, :as => :commentable

According to the Mongoid associations documentation, all embedded_in associations are polymorphic. You don't need the commentable_id and commentable_type columns mentioned in the Railscast when using Mongoid, because the comment is a child of the commentable. In the Comment model, add the following line:

embedded_in :commentable, :inverse_of => :comment

Setup the routes in config/routes.rb like this:

resources posts do
  resources comments
end

resources pictures do
  resources comments
end

Add the following method to your comments controller as a private method. This is identical to Ryan's method:

def find_commentable  
  params.each do |name, value|  
    if name =~ /(.+)_id$/  
      return $1.classify.constantize.find(value)  
    end  
  end  
  nil  
end

In each of your comments controller actions where you need to find the comment, call the find_commentable method first to get the parent. Once the parent has been found, you can find the comment by ID, by searching through the commentable's comments. For example, in the edit action the code to find the comment would look like this:

@commentable = find_commentable  
@comment = @commentable.comments.find(params[:id])

To reduce the repetition of calling find_commentable at the start of every action, you could put a before filter at the top of the controller like this:

class CommentsController < ApplicationController
  before_filter :find_commentable
  ...

And then change the return call in the find_commentable method to:

return @commentable = $1.classify.constantize.find(value)

I haven't encountered any problems using this method, but if you come across any issues please point them out.

like image 20
Chris Alley Avatar answered Jan 02 '23 13:01

Chris Alley