#154 Polymorphic Association (revised)
May 19, 2012 | 11 minutes | Active Record, Routing
A polymorphic association allows a model to belong_to different types of other models. Here I show how to make a single comment model belong to articles, photos, and events.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Is this not a good solution due to no foreign key references?
There is a problem with polymorphic associations if the polymorphic side has Single Table Inheritance: then it will only list the base class as the type.
polymorphic has_many is much more useful. that way THE SAME photo can belong to many different models.
Thanks Ryan! And I picked up another text mate tip, CTRL + Shift + H (create partial from template). Actually, do you reckon that could make for a good topic? TextMate tips?
I'd love to see this. My Textmate-fu is weak compared to Ryan's and I'm constantly picking up little tips from him. I'd love to see more in the same place.
Ryan: to get the type of commentable tpye and ID, I use the follow code:
and append to the nested comment resource this
, :defaults => { :commentable => 'picture' }
(when commenting on picture)
This way, the only thing you have to remember when adding more commentable is to add the default param to the route and you are done ;-)
This is a great solution.
+1
dude your comments are hilarious, i just looked at your profile
Super awesome... This solves my varying nested level polymorphic nightmare. It took me a while to sort this out, but after a few synapse cycles it finally makes sense. Thanks!
+1 for nested resources! I'm having trouble commenting though. When I tried create a new comment in something like users/1/nested/12/comments/new
I get redirected to
nested/12/comments and users/1/ disappears
and then I get error saying
undefined method `classify' for nil:NilClass
in my CommentsController#create
How do I solve this issue?
Is this a little dangerous even with receiving a default param from routes table? Is it possible for end user to set ?commentable='arbitrary_class' in a request to create object of any class?
Unfortunately the official Rails api is misleading when describing how to use url_for and consequently link_to with nested routes. " If you have a nested route, such as admin_workshop_path you’ll have to call that explicitly (it’s impossible for url_for to guess that route)." This isn't the case since you could use url_for([:admin, Workshop.new]) or similar. All you need to do is provide a symbol or an object that's above the target wrapped in an array. Thanks Ryan for pointing this out.
Ryan,
I think the
load_commentable
before filter used in this RailsCasts episode is somewhat brittle and, probably too complicated to be used as an example, especially when demonstrating this feature to new users.I would rather go with a simpler alternative. Something like:
Or even more verbose:
I see several benefits in this kind of code:
Code is less clever, which is a good thing for controller code.
Filter code is decoupled from changes to routes. For example, if a
scope
ornamespace
was added or thepath
option was used to produce easier to read routes, you would not need to change filter code or tests.Scopes can be added independently to each
find
call. Articles could bepublished
and eventsactive
, without the need for aliases.Code produces no temporary objects nor relies on string processing which, over time, add up to app execution time.
Tests are easier to follow and coverage easier to check.
I would also like to mention that, in real world, some of the most popular uses of polymorphic associations, like comments, are a bad pattern.
In this example, article comments will never be queried as event comments nor -most probably- listed together and, they even might end up having very different requirements. In Ruby, behavior can always be shared using modules.
Still, there are some valid uses for polymorphic associations but, way less than what one would think of first.
P.S. thanks so much for your work and contribution! I admit this is one of the very few cases where I don't agree with the code presented. :-)
Adrian: look at the solution I describe above. Way less verbose than yours and more flexible.
Alain, to me and in this case, being verbose is a feature, not a bug. Back to your example, I don't think a
commentable_id
really belongs to the controller. Most of my other comments apply to your code too.I still dont agree with your pattern. What happen when you have a nested article - picture - comment?
Your technique will see the article_id and load it as the commentable while we are trying to comment on the picture.
That being said, I agree that being able to add scopes is a plus and I find that it is your biggest argument.
The if-block method couples the find_commentable to the thing that is commentable. If you add another one tomorrow then you have to go add another case. While that's ok in an app with an app level controller, if you have something like a mountable engine, you'll have to pick a convention and stick with it, sans-special knowledge of 'all things commentable.'
Good, KISS(Keep It Simple & Stupid). +1
Can you expand on when you should and shouldn't use polymorphic associations? I was thinking of using them for :imagable photos on my applications (users, events, classes, projects, etc all have many photos each via Paperclip). Seems like a great way to make some very DRY code.
Adrian - thank you! this is exactly what I needed (I have a bit different routes).
and for me (starting with rails) verbose is an awesome feature :)
Thnx your solution works for my Dutch website where I rename my resources to Dutch words like so:
resources :users, :path => "gebruikers" do
resources :comments
end
Thank you Alain. I had nested resources with comments for both resources, and this worked for me.
for example:
Order from deepest nested resource down.
How did you deal with your form post? Was it to events_comments_path or events_pictures_comments_path?
Just put :comment_type => :article, or :events, :photo in the routes as a parameter inside of the resources block and then setup a before filter to classify that value (params[:comment_type]).
I have to agree that this cast was timed correctly for me as i was looking into something that could benefit from a polymorphic relation.
the other thing which i found useful is this code from stackoverflow, which shows how to set up a has many through polymorphic table
It looks like a great way to link locales to items for me
This is a great way to get the "other" side of a polymorphic has_many.
Why not just use a route like this?
I don't think that would be good practice. Some questions:
What about route path translation?
Why would you need a parameterized route when you won't be adding new models in runtime? It's just 3 cases, not infinite associations.
Lastly, this could potentially clash with other routes in your
routes.rb
file depending on your routes order.I have been working on a similar system except that mine is more complex because i have nested resources. I can't get past the first level of nesting with the polymorphic path in the form_for.
For example I have something like this:
Locations with many Building with many Rooms. I want to comment on each with the polymorphic relationship but the system fails when you try to comment on a Building Rails throws an exception looking for 'building_comments_path' but the routing table generates 'location_building_comments_path'.
I am at a loss to generate the polymorphic path on the fly. I could manually generate the routes or the path in my controllers but I figure there has to be a better solution.
Ryan,
As mentioned above my resources are nested. I would suggest one change:
resource, id = request.path.split('/').last(2)
Nice episode - had a quick question though. If I wanted to add comments to the index pages eg Articles#index (rather than Articles#show) how would I set up the instance variables @commentable, @comments, @comment?
Hey guys, how can I get the articles using comment object?
Somethin like these:
comment = Comment.all.first comment.article
supposing the first comment has a comentable_type as article.
Rayan,
Regarding to create a new comment on each model,
I have "news" model and "task" model which is nested under "project" model routing
Is there a way to accomplish it so I don't have two links?
There is a good answer here at Stackoverflow. Basically it makes use of
polymorphic_url
andpolymorphic_path
Typo in the /app/controllers/comments_controller.rb code block in 'Read the episode'
@commentable = resource.singularize.classify.contantize.find(1)
... missing the 's' in constantize
Having difficulty creating delete and edit links for the polymorphic object.
Anyone know of a good resource on that?
How would you destroy a comment?
I get:
NoMethodError in CommentsController#destroy
comments' for #Comment:0x000000030aae58`undefined method
comments controller:
I think you want to do this
I might be wrong.
There is an error in the ASCII version
def load_commentable
resource, id = request.path.split('/')[1,2]
@commentable = resource.singularize.classify.contantize.find(1)
end
should be:
def load_commentable
resource, id = request.path.split('/')[1,2]
@commentable = resource.singularize.classify.constantize.find(id)
end
change contantize to constantize and put id between parenthesis.
Thanks for letting us know, I've corrected the ASCII version
My understanding is that calling
singularize
beforeclassify
is redundant and can lead to problems with singular forms that end with "s". See http://apidock.com/rails/ActiveSupport/Inflector/classify and http://apidock.com/rails/ActiveSupport/Inflector/camelize. To be safe,classify
should only be called on a plural form. Thecamelize
method can be called on either and will return a result with the same grammatical number (i.e. singular or plural). Of note, the source forclassify
(at least in 3.2.8) is:Repeated calls to
singularize
are not necessarily safe:I have small problem.
I'm using polymorphic on my Review model as reviewable. Where users can review other users and posts.
How will I make it possible to have an owner for each review. BY owner i mean a associated user. Because owner User and the reviews on User are crashing. All good method i can follow or ideas ?
Hello I learned a lot from this episode about Polymorphic association, which always seemed a bit mysterious!
I think what the episode is really missing, is an explanation about polymorphic_url or polymorphic_path in order to recognize which model you wish to redirect. It is extremely helpful to understand this method especially when nesting multiple polymorphic models (see documentation). For example
sorry if messed up the comment markup ^^
Thank you Ryan for the wonderful videos. I have been watching your masterpieces for a few years. I love your Pro and Revised versions, Keep it up!
My next polymorphic task is to add this exact function to a small app I have. I thought I was starting easier to just have an address field shared across models but there's not much out there to help with that, especially for Rails 4. I have put together how I got an address polymorphic association working in my app at my blog if anyone needs that help.
http://hamcois.com/articles/6
I have a problem with nested forms and a polymorphic model. Using comments as an example, how does one add multiple has_many :through for the comments needed.
Example:
has_many :comments, :through => :images
has_many :comments, :through => :articles
The second has_many overwrites the first one and :through only allows one reference.
Any tips appreciated.
Never mind, must have been too little sleep. Using a class_name solves it.
How do I test this?
I tried:
But it doesn't work.
let(:comment) { FactoryGirl.create(:comment, user: user, commentable: question) }
subject { comment }
it { should respond_to(:commentable) }
Hey guys,
So in an attempt to get better at Ruby on Rails, I think it would be good for me to build the applications while watching the screen cast. I know Ryan has given us the before and after applications, but when i tried to start the server it showed just the basic information, meaning there was no data (texts or pictures). I am wondering if that is due to something i am doing or is it just not provided.
Thank you
Figured it out. Must do rake db:setup
Still a great episode.
If you're using a mountable engine, here's the best way to get the commentable working:
In your routes.rb:
In your comments_controller.rb:
For mountable engines, you HAVE to do
MyEngine::
when constantizing the object. Won't work without it.Extremly useful episode Thanks Ryan.
I'm currently using disqus.com for comments but I'm having SSO issues so I'm thinking of writing from scratch... Is latency an problem as the number of comments increase? Also, how can you get the comments to chain together? Thanks
This episode has been updated to Rails 5 as a blog post Polymorphic Association in Rails 5