#154
Mar 23, 2009

Polymorphic Association

Polymorphic associations can be perplexing. In this episode I show you how to set it up in Active Record and then move to the controller and view layer.
Download (15.5 MB, 8:52)
alternative download for iPod & Apple TV (10.5 MB, 8:52)

Resources

script/generate nifty_scaffold comment content:text commentable_id:integer commentable_type:string
rake db:migrate
class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

class Article < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

class Photo < ActiveRecord::Base
  has_many :comments, :as => :commentable
  #...
end

class Event < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

# comments_controller
def index
  @commentable = find_commentable
  @comments = @commentable.comments
end

def create
  @commentable = find_commentable
  @comment = @commentable.comments.build(params[:comment])
  if @comment.save
    flash[:notice] = "Successfully created comment."
    redirect_to :id => nil
  else
    render :action => 'new'
  end
end

private

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

# routes.rb
map.resources :articles, :has_many => :comments
map.resources :photos, :has_many => :comments
map.resources :events, :has_many => :comments
<!-- comments/index.html.erb -->
<div id="comments">
<% for comment in @comments %>
  <div class="comment">
    <%=simple_format comment.content %>
  </div>
<% end %>
</div>


<h2>New Comment</h2>

<% form_for [@commentable, Comment.new] do |f| %>
  <p>
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>

RSS Feed for Episode Comments 72 comments

1. David Lowry Mar 23, 2009 at 02:35

Perfect timing, sir!

Acts_as_[secret] you say... :)


2. Aditya Sanghi Mar 23, 2009 at 03:06

Hey Ryan, Our Monday morning friend,

You sound like you have a cold in this episode. Have hot milk or something and keep warm!

This episode is going to help the newbies in my office a lot, thanks!

Cheers,
Aditya


3. Deger Mar 23, 2009 at 03:36

@Aditya Monday morning friend? You are right, I come here every Monday, it becomes my hobby now. Cheers!
Thanks Ryan!


4. Fadhli Mar 23, 2009 at 03:41

Cool, that find_by_commentable method is a pretty smart piece of code.


5. Billee D. Mar 23, 2009 at 04:46

Thanks again, Ryan. I just read Obie's take on this subject in The Rails Way a few days ago (I skipped through many chapters in that book!) This will become even more relevant (and timely) once people start realizing the potential for schema-less databases.

Awesome. :-)


6. sthapit Mar 23, 2009 at 05:21

What you could also do is to have Article, Photo, and Event be STI subclasses and have their superclass has_many :comments. Both approaches work of course but newbies might want to also consider STI - it keeps the code cleaner for counter_cache, adding more common functionality (e.g. voting, page views counting etc.). I say this because we have a similar setup at socialect.com and debated for a long time before settling on STI instead of polymorphic associations. In our case Classes, Events, Places, Groups, Topics are subclasses and each of them has many votes, comments, pageviews, memberships etc so it was very easy to add that to just the superclass.


7. shm Mar 23, 2009 at 06:20

hi ryan,
very nice solution!

another approach i used in one of my projects is to define the 'commentable'-type (or "context_type") directly in the routes, as mentioned here http://blog.opensteam.net/past/2008/11/26/polymorphic_controller_nested_routes_polymorphic/

then you don't have to 'trust' the params-hash to return the correct "commentable" :)

greetings,
shm


8. amrnt Mar 23, 2009 at 09:52

Exercise, huh?!
LOL!!
thanks alot Ryan B.


9. George Yacoub Mar 23, 2009 at 11:01

Thanks a lot. keep it up.


10. Marc-Antoine Mar 23, 2009 at 11:28

I'm trying to implement something like this with this example:

articles = User.articles_with_a_comment_from_me

I'm having trouble to figure how to setup the polymorphic through association in the User Model. Can someone help me?


11. amrnt Mar 23, 2009 at 12:19

what if, I use in the same table 2 fields with *_id ?


12. Nick Mar 23, 2009 at 12:20

Never understood polymorphic associations, or really took the time to learn them. They just seemed too complicated.

After watching this i've refactored some of the code in my first rails app and it's so much nicer.

Thanks for all your work in making newbies like myself better coders!


13. wil Mar 23, 2009 at 17:40

you got twitter account, Ryan?


14. Joshua Moore Mar 24, 2009 at 01:19

Thanks for the great screencast. I was trying to solve a problem last week that I could have used this for! Great stuff.


15. Matias Mar 24, 2009 at 06:21

Wish that I had this two years ago! :-(

Nice screencast by the way! Keep up the good work :-)


16. Ryan Bates Mar 24, 2009 at 09:24

@Aditya, thanks. I do have a cold. It's partly allergies too.

@sthapit, STI wouldn't work well here because all three models (photo, event, article) have very different content. You wouldn't want them to share the same table.

@shm, thanks for posting that. I was debating which approach to go with while preparing this episode. Both ways should work well.

@Marc-Antoine, try something along these lines (completely untested):
http://pastie.org/425523

@amrnt, it will only become a problem if you're trying to pass both *_id fields as parameters in the routes. If it gets that complex then you may want to go with shm's solution mentioned in comment 7 above.

@wil, yes. My twitter name is rbates.


17. Markus Arike Mar 24, 2009 at 17:03

Ryan, great screen cast as usual. One puzzling error I'm getting in running the Episode 154 code that is confusing is on trying to submit a comment to an article, on line 17 of the Comments Controller I'm getting a NoMethodError in CommentsController#create (The error occurred while evaluating nil.comments).

I don't know if it my setup, Ruby 1.8.7 with Rails 2.3.2, but it is certainly strange.

As always thanks for all the work you put into Railscasts.


18. Paul Gatt Mar 24, 2009 at 19:20

Hi Ryan, I've got a question for you: is it possible to have polymorphic nested associations, so that, for example, you could create comments on each parent model's creation page?

Thanks, any help would be appreciated,
Paul Gatt.


19. Girish Mar 25, 2009 at 01:09

Thanks for the great post Ryan.
 
You forgot to mention routes helper polymorphic_url([@commentable, comment]).

You have very nice screencasts, please keep up the good work.
- Girish


20. Radoslav Stankov Mar 25, 2009 at 04:26

I really liked $1.classify.constantize.find(value) trick. Superb cast, as always :)


21. Erik Mar 25, 2009 at 08:56

Excellent screencast!

I need some help to create a comment destroy action. How to redirect back to commentable/[:id]/comments after deletion?


22. Bill V Mar 25, 2009 at 10:14

I'm trying to understand why this approach is valuable. Couldn't the same practical result be affected by including belongs_to statements for all three models in the comments model and passing the type and id values to the index and create methods as parameters?

I'm not arguing against it; it is very cool. I'm just trying to understand the benefit gained from the additional complexity.

Thanks. These screencasts have moved me several steps ahead of where I was.


23. Mina Naguib Mar 25, 2009 at 11:32

Regarding the redirect_to, there's no need to use the :id => nil hack. It can be more clearly done as so:

redirect_to [@commentable, :comments]


24. Chris Mar 26, 2009 at 06:36

Once again, your screencasts answer my question perfectly, going one-step further than most other tutorials on the web. The advice on where how to load the correct polymorphic model (find_commentable) is exactly what I needed to get my code working today.

Railscasts are undoubtedly the best rails tutorials on the web - keep up the great work!


25. Erik Mar 26, 2009 at 11:00

How to redirect back to @commentable index action?


26. Mina Naguib Mar 28, 2009 at 13:00

@Erik

Ugly but works:

redirect_to @commentable.class.new


27. Erik Mar 29, 2009 at 21:29

@Mina

Thanks!


28. Gavin Laking Mar 30, 2009 at 08:08

Hi,

I have a database which has three tables; Company, Customer and Shop. All three have various similar address fields (Address 1, Address 2 Postcode, Country etc.). I'm wondering if it would be wise to put the address parts into an Address model and use a polymorphic association to map addresses to Company, Customer or Shop. Is this a good idea?


29. Mina Naguib Mar 31, 2009 at 09:13

So, this episode inspired me to extract some code and release this:

http://github.com/minaguib/resourceful_parenting/tree/master

Essentially, it replaces the hand-rolled find_commentable, as well as helper functions for doing some of the redirects discussed in the comments here.


30. Philipp Apr 01, 2009 at 11:54

Hi,

Sry but the line $1.classify.constantize.find(value) doesn't work because in my routes there is the following line map.resources :news, :singular => :news_instance, :has_many => :comments so that I think that $1 isn't the right variable. But I don't have any better idea.


31. Sig Apr 01, 2009 at 23:03

Hi Ryan, awesome screencast as always.
You rock man !!!

I'm trying to implement polymorphic association with 2 level nesting structure.
Basically I have projects and documents; projects have tasks as a nested resource and documents have reviews as a nested resource. Tasks and Reviews must have a polymorphic association with prompts.

project/1/task/1/prompts
document/1/review/1/prompts

The issue I'm facing right now is what I have to put on form_for in order to make it works with the 2 level nesting. I should refer to project for task and document for review but how can I do that dynamically?

Any ideas is very welcome.

Thanks again for your outstanding reailscasts.


32. Frank Apr 06, 2009 at 15:27

Hi Ryan as ever your screencast rocks!
I have a question, simple i think but i cant figure out a smart solution.
I have a model Product and i wanna other 2 models (example, Cigarettes and Wine) that share some caracteristics of Product but have also things that are different from Product.
What is the best solution to create this two model? Create a polymorphic association for Product or, STI ?
Thanks.


33. Chris Apr 11, 2009 at 10:47

The problem with the polymorphic association technique is that it doesn't allow you to use foreign key constraints to ensure data integrity. I don't use it for that reason alone.

@Gavin - consider the party supertype/subtype model: companies and customers would both have parties as their supertype. Parties would then have many addresses. It's a very common pattern. Google it for more info.


34. Andrew Nordman Apr 11, 2009 at 20:10

I've been working on this same setup using upgrades for an analysis website, turning comments/upgrades into a polymorphic generic.

In the cast, you use a hack of :id => nil to get back to the page you were on before. This seems to only work if your creation form is on the index page. In my scenario, I kept the comment creation form on the new action. It took me a while, but I found a way to keep using RESTful routing and the polymorphic generic
controller.

The code: http://pastie.org/444106

Since we're storing the type, we use it to form a string version of the RESTful route, then eval() in the redirect_to. I'm not sure if it's any less 'hackish' than the :id => nil, but it's at least a bit more versatile in where you're coming from before the create action.

Just thought I'd throw out how I did it for folks that might stumble into the same situation.


35. Bharat Apr 14, 2009 at 13:20

OK. I think I am doing something silly. I downloaded the code, ran migrations and am trying to enter data to test this. When I enter a new Article, the article gets created, but not the comment and I get an error as follows:

ActiveRecord::UnknownAttributeError (unknown attribute: article):
  app/controllers/articles_controller.rb:8:in `new'
  app/controllers/articles_controller.rb:8:in `show'
  /usr/local/lib/ruby/gems/1.8/gems/ruby-debug-ide-0.3.4/lib/ruby-debug.rb:96:in `debug_load'
  /usr/local/lib/ruby/gems/1.8/gems/ruby-debug-ide-0.3.4/lib/ruby-debug.rb:96:in `main'
  /usr/local/lib/ruby/gems/1.8/gems/ruby-debug-ide-0.3.4/bin/rdebug-ide:76
  /usr/local/bin/rdebug-ide:19:in `load'
  /usr/local/bin/rdebug-ide:19

here is the line of code in the articles_controller in show action:

    @comment = Comment.new(:article => @article)

Anyone else get that, how should I fix it?

Thanks.

Bharat


36. Bharat Apr 14, 2009 at 13:49

Hello Ryan,
The code for this episode seems to be horribly broken? I have references to site_url in the show.html.erb file for articles and that is not even an attribute for the article or comment? Can you or anyone else help? I feel quite lost frankly. Is there something that I am missing? It seems like no one else has these errors show up? I downloaded the code from Github, wrong place?
Please help
Thanks.
Bharat


37. Dario Apr 14, 2009 at 20:05

Hi Ryan,
same problem here.
How can we fix it?

Thanks
Dario


38. Mina Naguib Apr 15, 2009 at 21:24

@Andrew Nordman
or you could just
redirect_to [@commentable, :comments]

or through my resourceful_parenting plugin (which relieves you from even writing find_commentable):
redirect_to [parent, :comments]
or
redirect_to polymorphic_p_path(:comments)

Whatever you find more readable/maintainable.


39. Gavin Laking May 07, 2009 at 03:42

@Chris: Sorry, for the delay in replying, only just remembered I posted this (as I moved on to working on a different problem!). I'll have a look at the Party super and subtypes; didn't think of that. :-)


40. Toggo May 08, 2009 at 03:22

I've followed this tutorial, but I have added another column to the comments model 'name', I'm not sure how to access this though in my view, as: article.comment.name, and variants of that give me the old undefined local variable or method error. Can anybody help?


41. Gavin Laking May 08, 2009 at 07:53

@Toggo: Have a look at the gist. http://gist.github.com/108808


42. Gabor May 16, 2009 at 00:42

Is there a way to to emulate an is_a relationship with RoR? i was thinking some combination o STI and polymorphic. I'm trying to do "agenda has_many 'subtasks'".
Task :id :name :index :subtask_type :subtask_id
SubTaskFoo :id
SubTaskBar :id


43. Lauren May 18, 2009 at 22:44

Hi I am very new to Rails and these Railscast have been awesome.
Just one thing I am hung up on, I am having trouble with my redirect.
You link to "Add Comments" from
/installations/1/onposts/2/ it goes to /onposts/2/comments
The problem is once someone has added a comment I want it to redirect back to the
/installations/1/onposts/2/. Any help would be greatly appreciated.
Thanks


44. Andrew Skegg May 19, 2009 at 03:27

I cheat a little when creating polymorphs by using:

./script/generate model comment content:string commentable:references

Still have to go back and add the ":polymorphic => true" parts, but every little bit helps I guess.


45. Aninda May 20, 2009 at 11:52

recommended usage : $1.pluralize.classify.constantize

classify only handles pluralized names. so 'bussiness'.classify would yield 'bussines'


46. Daniel Mazza May 22, 2009 at 22:47

Great screencast, Ryan.

@Bharat, @Dario

I found an error (line 8),
http://github.com/ryanb/railscasts-episodes/blob/287ea29922e0b5d9f5b3c7db0b6a3a433d5adfc7/episode-154/blog/app/controllers/articles_controller.rb

It should be:
http://pastie.org/487142

#-----
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @comment = Comment.new(:commentable => @article)
  end
  #...
end
#-----

With ":commentable => @article",
not ":article => @article".


47. Luke Jun 09, 2009 at 22:50

Thank you, this is exactly what I was after!!


48. Yuri Tomanek Jun 12, 2009 at 03:16

Has anyone been able to work out the issue with the error -

"You have a nil object when you didn't expect it!
The error occurred while evaluating nil.comments" ?

I am getting the error in my code and the example code on railscast github.

Would love to work out a solution but have not been able to myself.


49. Deb Prado Jun 18, 2009 at 17:09

@Yuri

I'm having the same problem. Were you able to work it out?

thanks

Deb


50. m4rtijn Jul 12, 2009 at 05:06

I found the solution to the create action error: line 17 nil.comments.

In the create action you need to add:

def new
  @commentable = commentable_find
  @comment = Comment.new
end


51. m4rtijn Jul 12, 2009 at 05:06

sorry, I meant in the NEW Action you need to add that


52. Alex Jul 15, 2009 at 15:38

What about many-many polymorphic relationships?

If I have a Task model, and I want it to collect multiple Targets, how would I go:

task.targets
or
target.tasks

To me polymorphic relationships are a great way to approach mixed result sets without having to use STI.

I define an "abstract" model which defines the relationship to the polymorphic-collection. Then, every time I extend it, that class is eligible to participate in the polymorphic relationship.

Is there a way to do this without creating a two way dependency between the classes? Specifically, is there a way to do it without having to tell task about every target type it will ultimately collect?
I'd love to be doing this properly!


53. Andrew Latimer Jul 20, 2009 at 01:53

Is there a way to display the comments on the page for each model (Article, Photo, Event)? Preferably, I'd like to create a universal partial for all the models to share, but I'm having trouble in doing so.

I keep getting the error "undefined method `comments_path' for #<ActionView::Base:0x38d3c24>" when I attempt to show the comment in the view for each model. Adding the comments resource back into the routes file allows me to view the comments as I would like to, but creating them redirects to /comments instead of /articles/1 like I would like it to.

Any help?


54. matix Aug 19, 2009 at 08:24

@Sig: I found nice solution to use Polymorphic Association to more than 1 lvl url: http://pastie.org/588592

Greetz, Rails rocks ;)


55. Val Aug 23, 2009 at 16:46

I downloaded your code and just after creating the first article I got - ActiveRecord::UnknownAttributeError (unknown attribute: article):
  app/controllers/articles_controller.rb:8:in `new'
  app/controllers/articles_controller.rb:8:in `show'
  -e:2:in `load'
  -e:2

Rails 2.3.2 Ruby 1.8.7


56. Val Aug 25, 2009 at 10:02

I fixed all bugs and working code you can download from
www.rubyf.info/files/polimorphic_work0.zip


57. emou Sep 24, 2009 at 06:11

This was a great episode! Thank you.


58. Rob Jones Sep 25, 2009 at 15:53

Sorry - but this does not work for me. The code on github appears to be just broken (e.g. the @comment = Comment.new(:article => @article) line referred to above.

I may be being very dumb but find_commentable has to return 'Commentable' as the parameter name is commentable_id right? Or do I just not get the route magic..


59. Rob Jones Sep 25, 2009 at 16:25

OK - I get it - if, and only if, you use the nested urls (articles/1/comments) then the magic works. If you try and move the comment into an article page directly then it doesn't.


60. Description at stackoverflow.com Sep 26, 2009 at 15:40

Hi, guys. If you still read these comments, than may I ask you to help me with a problem with polymorphic association? Thanks


61. Andrei Sep 26, 2009 at 16:35

My bad! I haven't changed the routes -- that was the problem of the comment #93. Now it works.

Thanks to Val for the corrected example (comment #59).

Ryan, what are you going to do with all these spammers?


62. Tomás Senart Sep 27, 2009 at 11:15

Since Rails 2.3.4, habtm join tables can't have a primary key. That broke my app. How can I make many-to-many double polymorphic associations now? I have a relationships table with:

  def self.up
    create_table :relationships do |t|
      t.column :origin_id, :integer, :null => false
      t.column :origin_type, :string, :null => false
      t.column :destination_id, :integer, :null => false
      t.column :destination_type, :string, :null => false
      t.column :position, :integer, :null => true
    end
    
    add_index :relationships, [:origin_id, :origin_type, :destination_id, :destination_type],
              :unique => true,
              :name => 'index_relationship_polymorphs'
  end

Any idea of how to accomplish this?


63. Javix Nov 23, 2009 at 04:15

@Val, comment #56. Even after trying the modified code, if you come to http://localhost:3000/articles/1, you'll get an error: ActiveRecord::RecordNotFound in ArticlesController#show

Couldn't find Article with ID=comment1 for article 1.

Thanks to Ryan, of course it's a good tuto, but I am asking myself: how can one post a tuto without event checking if it works, - so let the 'pupils' find their way themselves? There is too much to check and find why it doesn't work!


64. Javix Nov 23, 2009 at 04:23

the error is in 'articles/show' page, line 17:
<%= link_to_unless comment.content.blank?, h(@article.author_name), h(comment.content) %>


65. Javix Nov 23, 2009 at 05:09

undefined method `site_url' for #<Comment:0x4a8993c>, - that is No helper in the source code found!


66. JPB Nov 24, 2009 at 14:00

Thanks for the great episode, Ryan. I would like to make a note here for anyone else stuck on paths for the comment resource. Typically, you would use comment_path(comment) in your partial with map.resources :comments in your routes.rb. Or if it was a nested resource, article_comment_path(article, comment) would work; but we can't do that here, because we don't know if we should use article_comment_path, photo_comment_path, or event_comment_path!

The solution is to use polymorphic_path([comment.commentable, comment]), as mentioned in Module
ActionController::PolymorphicRoutes. Hope that helps someone else out.


67. Javix Nov 25, 2009 at 07:23

@JRB: Can you explain how to resolve the error with 'unknown' method site_url in articles/show page and where to put polymorphic_path([comment.commentable, comment]) as you adviced in your post? Thank you.


68. wholesale nike shoes Jan 14, 2010 at 18:01

A very good article, I will always come in.


69. fashion scarves Jan 14, 2010 at 18:02

Such a good article, caught my sympathy!
-


70. Sean Jan 19, 2010 at 13:16

What is the best way to, on error submitting a comment, render the original article/photo/etc page and show the error?

What would the render line be?

Aka without polymorphism you can do:

   render 'article/show'

but now you would have to have a case for each class type... or build the location via the class name...


71. Nike Air Max 90 Jan 24, 2010 at 21:08

Thank you for sharing.Nice post.


72. javon Jan 25, 2010 at 22:36

Nice post.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source