#154 Polymorphic Association
Mar 23, 2009 | 8 minutes | Active Record, Routing
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:
- source code
- mp4
- m4v
- webm
- ogv
Cool, that find_by_commentable method is a pretty smart piece of code.
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. :-)
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.
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
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?
what if, I use in the same table 2 fields with *_id ?
@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.
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.
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.
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
I really liked $1.classify.constantize.find(value) trick. Superb cast, as always :)
Excellent screencast!
I need some help to create a comment destroy action. How to redirect back to commentable/[:id]/comments after deletion?
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.
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]
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!
How to redirect back to @commentable index action?
@Erik
Ugly but works:
redirect_to @commentable.class.new
@Mina
Thanks!
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?
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.
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.
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.
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.
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.
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.
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
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
Hi Ryan,
same problem here.
How can we fix it?
Thanks
Dario
@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.
@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. :-)
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?
@Toggo: Have a look at the gist. http://gist.github.com/108808
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
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
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.
recommended usage : $1.pluralize.classify.constantize
classify only handles pluralized names. so 'bussiness'.classify would yield 'bussines'
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".
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.
@Yuri
I'm having the same problem. Were you able to work it out?
thanks
Deb
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
sorry, I meant in the NEW Action you need to add that
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!
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?
@Sig: I found nice solution to use Polymorphic Association to more than 1 lvl url: http://pastie.org/588592
Greetz, Rails rocks ;)
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
I fixed all bugs and working code you can download from
www.rubyf.info/files/polimorphic_work0.zip
This was a great episode! Thank you.
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..
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.
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?
@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!
the error is in 'articles/show' page, line 17:
<%= link_to_unless comment.content.blank?, h(@article.author_name), h(comment.content) %>
undefined method `site_url' for #<Comment:0x4a8993c>, - that is No helper in the source code found!
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.
@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.
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...
Hi Ryan,
I have a query for you with regards to this episode. I am referencing my pages with a "slug" column. and want to be able to do pages/slug/comments
I have tried saying for it to use the slug as a variable to pass everything through but it seems not to want to work. any thoughts?
Hi Ryan,
I whole heartedly thank you for the community service that you are doing. And many congratulations for completing 3 yrs on it and still going :)
A small request from my side. Please consider doing a screencast on has_many_polymorphs
Thanks!
Hi,
I have a problem:
---
In view blog/show.html.erb
<div id="comments">
<% for comment in @comments %>
<div class="comment">
<%= simple_format comment.comment %>
</div>
<% end %>
</div>
<h2>New Comment</h2>
<% form_for [@commentable, Comment.new] do |f| %>
<p>
<%= f.label :comment %>
<%= f.text_area :comment, :size => '25x5' %>
</p>
<p><%= f.submit 'Submit' %></p>
<% end %>
---
After create the submit redirect_to blog/:id/comments
how redirect_to blog/:id ??
Best Regards.
Thanks!!
When i try to run the migration i just get a MYSQL error. Can't create table. I'm guessing it has to do with foreign keys to a non exiting table (commentable) ?
In find_commentable, it might be better to use:
$1.pluralize.classify.constantize.find(value)
or
$1.titleize.constantize.find(value)
I ran into problems when classify was called on a model like 'business', yielding 'busines' and throwing fun little errors.
Not everyone needs it but it's a caveat that is annoying at best...
As always, thanks Ryan!
Redirect from polymorphic CREATE and DELETE action are here http://gist.github.com/440723
I know this is an old cast, but did anyone ever figure out how to post a comment from just /controller/id instead of controller/id/comment/
@Bharat
Just change line of code in the articles_controller in show action:
@commentable = @article
@comment = Comment.new
@comment = Comment.new
@Phillip (I know, more than a year later):
I think your issue is the same as mine. For example, in Ryan's example, if Photos were nested in Articles, find_commentable would not work correctly. Assuming a RESTful API, then you need to retrieve $1 from the request.request_uri (note my example uses 'imageable' as opposed to 'commentable'):
def find_imageable
pathArray = request.request_uri.split('/')
if pathArray.count > 2
if pathArray.count % 2 == 0
imageableString = pathArray[pathArray.count-3].singularize
else
imageableString = pathArray[pathArray.count-4].singularize
end
@which_id = imageableString+"_id"
if pathArray.count % 2 == 0
return imageableString.classify.constantize.find(pathArray[pathArray.count-2])
else
return imageableString.classify.constantize.find(pathArray[pathArray.count-3])
end
else
params.each do |name, value|
if name =~ /(.+)_id$/
@which_id = name
return $1.classify.constantize.find(value)
end
end
end
nil
end
Anyone been able to do this with RJS? I don't know what to put in the "form_for" since this is not in the new action of the comments but in any show of the articles, photos, etc
I'm using rails 3.0.7 . As I was getting through this awesome cast I couldn't make my url
localhost:3000/article/2/comments
work:I spend hours trying to find out...later I got the solution ...
in
routes.rb
file i did:and then only
localhost:3000/article/2/comments
workedthis is almost exactly what i needed. Thanks, Ryan. One question though. I have a few models, and they can has_many :messages, but i'm creating these messages in an after_create callback, and not via a form, so i'm doing a Message.create in my callback, so question is in my messagable_id column do i simply put the id of the instance passed to the callback and messagable_type is just the class name?
I also had the issues with
classify
not recognizing singular names like "business", so I went withHi, I would write the create method like this:
Please 'revisit' this for Pro subscribers!
The sortable list revised screencast was GOLD.... would really appreciate a Rails 3.1 working example of polymorphic associations for Rails 3.1....
To adapt the magic sauce with nested resources :
i did :
which allow to find the last controller param with its Id , and for me works well to get the @commentable type and id
Hope it helps someone, cheers
Please redo this for Rails 3, even adding in namespace help too would be brilliant! Struggling with this at present...
Any help would be appreciated
great screencast but i also had issues with plural names like statuses would read statu ... but thanks to @Adam for correction of classify to titleize . Cheers !
Great screencast! Is it possible to add count of subelements to json/xml output? e.g. photo has many comments, and I want to add comments_count to /photos.json ?
Great screen cast!
Great Screen Cast Ryan
@pradeepbicc, Thats called as Nesting of routes.
Hello! noob here, how do you add a "remove" item for this?? Thanks!
I needed to validate permission of a polymorphic association with CanCan.
Hope this helps someone:
Better explanation in this Gist.
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
Always raises the exception, it seems it doesn't get the commentable_id parameter, so what can i do to make this work.
Hi, how can I create a new Comment inside my _form.html.erb of any model? I don't want to go to localhost:3000/products/1/tags to create a new tag, I would like to create it inside new.html.erb view of any model. I have problems with the line:
<% form_for [@commentable, @comment] do |f| %>
I was wondering if any one knows of good solutions for polymorphic HABTM relations. Like the relationship between tags products and articles. I've seen some decent hacks involving join tables but they all require hard coding each evolved model in the join table.
Somebody tried to write specs for this episode?
Hi @FlintMakal - how about this for nested resources instead?
Thanks Ryan.
There is a syntax error in the file index.html.erb
<% form_for is give it should be
<%= form_for
params doesn't return all of this now (Rails 4.2). params only returns the action and controller and an id if applicable.
What do you do now? Parse the url?