Skip to main content

Comments With Polymorphic Associations Discussion

General • Asked by Chris Oliver
2f93c896dd8c2003922ad854adae209d

Awesome Video. I really liked and refreshed my concepts about Polymorphic Associations. Thanks :)


436c0cf6fac876442c98ece1bafed9c5

Here is a Pro episode suggestion - take this and add nested comments and some AJAX to it :)

7a20921c83940ce92363d28a6d84a712

Id be sure to subscribe for that. Not found a decent example of nested before.

436c0cf6fac876442c98ece1bafed9c5

It took me forever to build but I did it from scratch a few years back on a project that I still have yet to finish. I works slick but I am sure it could be done better than the way I did it. I used the ancestry gem and a few Railscasts episodes to cobble it together.

578712174c45171ce625b844844ad5ae

Maybe you should make a video on this. Some of us are still struggling through it.

Ce795239ba5dd2384fc2f88ffaff5451

I'm more than a little behind, but I added this to my short list!

5a00acdae79c03d7566e8f8e87fa9c08
50c7175b91eeaf97f25fd4ad1f8be4f0
C804b6d0315e6332e231295349f90485
+1 on the suggestion. Daniel's link is helpful, but I'm getting caught up on the polymorphic_url generation to use the _comment partial as a shared file between mutliple models. 

5122539f8ed040656ad6bf4bc32ae21c

Polymorphism is the main anti-pattern in rails(


97bb24e0eeee71854d6fc8664743e336

How would you suggest getting all the comments a user's posts collectively have? For example @user.comments.all throws the error
ActiveRecord::HasManyThroughAssociationNotFoundError at / Could not find the association :commentable in model User


Fd90804f2ddae21d542d9ec9e9da3ba3

Is this something to consider in a social networking site? If I would have say comments polymorphically associated with various models, wouldn't it be a major hit on one table all the time?

Ce795239ba5dd2384fc2f88ffaff5451

Since they are reads, I think that so long as your database server can handle it, it doesn't matter how many connections read from the same table. Reads don't lock the table/row like writes do since a read cannot cause data loss. You shouldn't experience any loss of speed if many things are reading the same table.


8133ee52ca72188132b5c7d97dfb967f

The problem with polymorphic models and tables is there is no way to keep the database from becoming corrupt due to no FK constraints.


20eb62fae359b9e23af7f26daf0e684d

I would suggest that you please either put in minimal security on these screen casts or at least mention that the implementation is very insecure and to check out the rails security guidelines. I know the case can be made where security is out of scope for this discussion but a, at one point you reference how this method is "more secure" when talking about the hidden form field and b, you leave xss/injection etc all wide open so that body may be used to by malicious users to extend reach.

0ea747d651c21d4028eafd3e12e302f5

Can you please elaborate on this part `you leave xss/injection etc all wide open so that body may be used to by malicious users to extend reach`? I'd like to understand the security risk a little more.


0ca89ebf7d7ca13c3076b538bdd79e16

How would this be handled if we only want the user to see their own comments, but not anyone else's?

Ce795239ba5dd2384fc2f88ffaff5451

You could do that by scoping the comments further by adding ".where(user: current_user)" to the query in the controller.


0ea747d651c21d4028eafd3e12e302f5

I found some of this information useful on how to test these polymorphic comment features at https://github.com/thoughtb...


Dec8133f91987ce7d9bbb1bdb83f5eb9

Interesting way to do this. I created an app that does comments, but I did it by the whole post has_many :comments, comments belongs_to post, and resource nesting. Took me hours to figure out how to do display comments. What's the pros and cons of the that way versus this way?

One great thing about this episode is that the whole polymorphic thing finally clicks for me, because for the longest time I still didn't really understand it despite reading on it repeatedly. In all of my albiet smallish projects I never used it, now that I understand it finally, I have some idea when to use polymorphic association.

Thanks so much! I signed up for GoRails based on this episode and the omniauth twitter episode!

Ce795239ba5dd2384fc2f88ffaff5451

The main difference is that you probably have tied your comments to the Post model. With polymorphism, you could have comments on the Post model, the User model, or any other model you've got.

And thank you a bunch for subscribing!!

Dec8133f91987ce7d9bbb1bdb83f5eb9

Ah I see, what would be the pros and cons of each solution in your opinion?

Ce795239ba5dd2384fc2f88ffaff5451

If you don't go with polymorphism and you want comments on multiple models, you will need to have two different tables for comments, like PostComments and UserComments, but why do that when you could combine them into just Comments? Really that's the main difference. You reduce duplication there.

Dec8133f91987ce7d9bbb1bdb83f5eb9

Ah I see that makes sense. What about foreign key and rails integrity? Is that ever an issue?

Ce795239ba5dd2384fc2f88ffaff5451

You shouldn't have any trouble. Rails handles it well. But you're right that with polymorphism you don't get the same database level enforcement like you would with the individual associations.


65c9bdc1d992924104da8ca25f000944

How do you add a destroy method to this


Acba3e7061eb78229a212161054be04b

I am trying to add a delete comment button to this. But it is sending me to the articles controller destroy action. Please can you add how to add a delete button to the comment? I'm stuck. Thanks very much!

Ce795239ba5dd2384fc2f88ffaff5451

Hey Melanie!

For deleting comments, it might be useful to add a "resources :comments" to your routes that isn't inside another resources block. Then you can create a regular CommentsController with a destroy action like normal. That way you can delete any comment as long as you know the ID of it and not worry about whether it's a film or actor comment because that doesn't really matter when you're deleting a comment.

To add the delete link to each comment in the view, you can say: <%= link_to "Delete", comment, method: :delete %> and that will make a DELETE request to the /comments/1 url, which will trigger the destroy action.

That should do it! If you want to go over and above, you can also make it as a "remote: true" link so that you can return some JS to remove the item from the page to make it AJAXy and nicer to use.

Acba3e7061eb78229a212161054be04b

Hi Chris. Thanks very much for the response. I tried adding this to my routes:

resources :comments, only: [ :destroy]

and this to my existing comments controller (which only had the create method in it - per your tutorial)

def destroy
@comment.destroy
respond_to do |format|
format.html { redirect_to data_url }
format.json { head :no_content }
end
end.

When I try this, I get an error that says:
undefined method `destroy' for nil:NilClass

Any ideas on what's gone awry?

Ce795239ba5dd2384fc2f88ffaff5451

You might be needing to add a before_action for the destroy action called set_comment to set the @comment variable. It's saying that @comment is nil there so that would be it. You can just set @comment = Comment.find(params[:id]) in the before action and you should be set.

Acba3e7061eb78229a212161054be04b

Thanks so much Chris.


Acba3e7061eb78229a212161054be04b

Hi Chris, I'm still struggling along in trying to get this set up. My current issue is with my comments policy update function. Your tutorial shows how to define update as set out below (which I have tried in both my article policy and my comment policy:

Article Policy:

def update?

#user && user.article.exists?(article.id) -- I have also tried this on this suggestion of someone on stack overflow).

user.present? && user == article.user

end

Comment Policy:

def update?

user.present? && user == comment.user

end

I keep getting a controller action error which says: undefined method `user' for #<class:0x007f9e24fa7cf0>

It highlights the update definition. Has something changed in pundit that requires a different form of expression for this update function? Can you see what might have gone wrong? Thanks very much

Ce795239ba5dd2384fc2f88ffaff5451

That looks correct. It sounds like the comment object is potentially the class and not the instance of the comment.

Does your controller have " authorize @comment" in it? And is your @comment variable set to an individual record?

Acba3e7061eb78229a212161054be04b

Yes - my articles controller update action has:

def update
# before_action :authenticate_user!
authorize @article
respond_to do |format|
# if @article.update(article_params)
# format.json { render :show, status: :ok, location: @article }
# else
# format.html { render :edit }
# format.json { render json: @article.errors, status: :unprocessable_entity }
# end
# end
if @article.update(article_params)
format.json { render :show, status: :ok, location: @article }
else
format.json { render json: @article.errors, status: :unprocessable_entity }
end
format.html { render :edit }
end
end

Ce795239ba5dd2384fc2f88ffaff5451

Do you have the part that sets @article?

Acba3e7061eb78229a212161054be04b

Hi - what part would that be?
def set_article

@article = Article.find(params[:id])

authorize @article

end

Ce795239ba5dd2384fc2f88ffaff5451

That's exactly what I need. I might suggest removing the authorize @article in this function and keeping the one in your update action instead. Everything else looks right, so you might need to also send me the full error logs to see where exactly it broke.

Acba3e7061eb78229a212161054be04b

The error log says:
Completed 500 Internal Server Error in 56ms (ActiveRecord: 28.7ms)

ActionView::Template::Error (undefined method `user' for #<class:0x007f9e1a734af0>):

22: <%= comment.created_at.try(:strftime, '%e %B %Y') %>

23: </div>

24:

25: <% if policy(Comment).update? %>

26: <%= button_to 'Edit', polymorphic_path([commentable, comment]), :class => 'btn btn-large btn-primary' %>

27: <% end %>

28: <% if policy(Comment).destroy? %>

app/policies/comment_policy.rb:13:in `update?'

app/views/comments/_display.html.erb:25:in `block in _app_views_comments__display_html_erb___34367520595054411_70158511210960'

app/views/comments/_display.html.erb:12:in `_app_views_comments__display_html_erb___34367520595054411_70158511210960'

app/views/articles/show.html.erb:68:in `_app_views_articles_show_html_erb__2619839868106814513_70158550563960'

Ce795239ba5dd2384fc2f88ffaff5451

Ah ha! So your errors is in the view, not your controller. :)

Change line 25 to say <% if policy(@comment).update? %>. Like I first guessed, you were referencing the class Comment, and not the individual record "@comment". That should do it!

Acba3e7061eb78229a212161054be04b

Hi Chris - when I try that, I get this error: Pundit::NotDefinedError in Articles#show

Showing //app/views/comments/_display.html.erb where line #25 raised:

unable to find policy of nil

Also, It's strange that the form of expression the way I had it works in delete, but not in update

Ce795239ba5dd2384fc2f88ffaff5451

It sounds like you're passing in a variable that's nil then. Are you sure you're before_action :set_comment is being called?

Acba3e7061eb78229a212161054be04b

Not sure how to check that. It's in the controller. I've followed the steps as set out in your tutorial. I'm not sure what I've messed up. I'll go back and watch the video again (take 30 might do it).

Ce795239ba5dd2384fc2f88ffaff5451

I think you're passing in that variable into a partial actually(?), so that wouldn't actually solve your problem. You may need to make sure you're passing the right comment variable into your pundit stuff in each case.

Acba3e7061eb78229a212161054be04b

If I change the view so that it's:

<% commentable.comments.each do | comment | %>

<div class="well">

<%= comment.opinion %>

<div class="commentattributionname">

<%= comment.user.full_name %>

</div>

<div class="commentattributiontitle">

<%= comment.user.formal_title %>

</div>

<div class="commentattributiondate">

<%= comment.created_at.try(:strftime, '%e %B %Y') %>

</div>

<% if policy(comment).update? %>

<%= button_to 'Edit', polymorphic_path([commentable, comment]), :class => 'btn btn-large btn-primary' %>

<% end %>

<% if policy(comment).destroy? %>

<%= button_to 'Delete', polymorphic_path([commentable, comment]), method: :delete, :class => 'btn btn-large btn-primary' %>

<% end %>

</div>

then the delete still works (it did with 'Comment' instead of 'comment', but the update shows an error as:

No route matches [POST] "/articles/10/comments/29"

In my routes, I have:

resources :articles do

collection do

get 'search'

end

resources :comments, module: :articles

end

resources :comments, only: [ :update, :destroy]

Ce795239ba5dd2384fc2f88ffaff5451

You must link to the route that matches the destroy action. You're linking to a nested route, but you want to link to comments route directly.

Replace the polymorphic_path([commentable, comment]) in your button_to's to simply be comments_path(comment)

Acba3e7061eb78229a212161054be04b

When I try:

<% if policy(comment).update? %>

<%= button_to 'Edit', comments_path(comment), :class => 'btn btn-large btn-primary' %>

<% end %>

I get this error:
Routing Error

No route matches [POST] "/articles/8/comments/30"

I'm baffled by the idea that update is defined in the same comments controller as delete. So far delete is working (using the polymorphic nested path), but update has errors.

Ce795239ba5dd2384fc2f88ffaff5451

Well, that's a button_to, which is a POST, but update is actually an UPDATE request. You'd need to add the :method => :update to the button_to here.

Your destroy action should be put inside the regular CommentsController and should be in the same place as these. The only reason you need the nested routes and controllers is for helpers that make creating the comments (and referencing the original object you're commenting) on a little simpler. To destroy any comment, it doesn't matter what the original object was, you can just delete the comment itself if that makes sense.

Acba3e7061eb78229a212161054be04b

Hi, still no good. Each of the create, update and destroy methods are in the ordinary comments controller. When I try:

<% if policy(comment).update? %>

<%= button_to 'Edit', comments_path(comment), :method => :update, :class => 'btn btn-large btn-primary' %>

<% end %>

<% if policy(comment).destroy? %>

<%= button_to 'Delete', polymorphic_path([commentable, comment]), method: :delete, :class => 'btn btn-large btn-primary' %>

I get this error:

undefined method `comments_path' for #<#<class:0x007f9e26697c30>:0x007f9e1a6daff0>

On top of that, now my delete action doesnt work either (it did prior to this set of changes).

Acba3e7061eb78229a212161054be04b

Should I go back to capital 'C' in comment for destroy? e.g.: <% if policy(Comment).destroy? %>

Ce795239ba5dd2384fc2f88ffaff5451

No, you'll still want that to reference the variable and not the class.

Do you have a github repo I can show you some fixes for this? It's kinda hard to give guidance in the comments. :)

Acba3e7061eb78229a212161054be04b

Hi Chris - I appreciate your efforts to help. The repo is private so I can't share it. Thanks anyway. Ill keep trying. It's been 3 years trying to grasp the basics and I'm still waiting for the penny to drop. Thanks anyway.

Ce795239ba5dd2384fc2f88ffaff5451

You'll get there! I think you can add me as a collaborator to the private project temporarily if you wanted, but no worries if not.

I would also recommend playing a lot with plain Ruby because that will help wrap your head around all these things in Rails. It's mostly just regular old Ruby code just connected in various ways. The Ruby Pickaxe book and Metaprogramming Ruby are both really good.

Feel free to shoot me some emails as well! My email's on the about page I believe.


Cf1cd18da4c066df7488d6c482a3c222

Chris, Fantastic episode here. Thank you very much.

I got it to work - mostly - in my set-up but I encounter a little redirect problem.

In my case my association is a "noteable" - referring to Notes.

My resources and namespace profile is:

namespace :navigate do
--resources :boks, :only => [:show] do
----resources :tools, :only => [:show, :index]
------resources :notes, module: :processus
----resources :processus do
------resources :notes, module: :processus

The problem I have is with the NotesController for the create/update of the notes. When I try to do a redirect I am finding that I am missing the information about the "bok" entity.

In my controller I have this:

def create
@note = @noteable.notes.new note_params
@note.user = current_user
@note.save
redirect_to [:navigate, @bok, @noteable], notice: "Your note was succesfully created."
end

Really the redirect_to should send me back to the right place, however the @bok entity is not present at all...mostly because at this stage I don't actually need it.

What would be the recommended approach to dealing with this nested situation?

Thanks!


A8239a0b67a6ea7b3a98d467e6bdfe6e

Chris, awesome episode! I'm trying to implement something similar, but before I dive deep into it I'd like to make sure I go down the right path. So I will do the exactly same, except comments are gonna have replies. My guess is if I have the right polymorphic setup for the comments, then I can just setup a simple `has_many + belongs_to` relationship between the comment model and the reply model, so from the reply's perspective it doesn't matter if the comment is polymorphic or not since every comment will have its unique id. Is this right?

Ce795239ba5dd2384fc2f88ffaff5451

Yeah, you can have a comment as a commentable, allowing you to have Comments with comments if you like. That's how you would normally setup threaded comments. You might want to put some limits on the nesting so you don't get threads that are too far nested in. Facebook limits it to one layer for example.


9d0fb019a19d89eba752803e7fa33d7d

In the comments controller when I try to do

@comment.user = current_user

Throws an error (no user method for user class) unless I do

@comment.user_id = current_user.id

Problem is, when I try to show the username associated to a comment (I need to do queries instead of accessing the object). Any idea on how to fix this?

9d0fb019a19d89eba752803e7fa33d7d

Oops, I never did the belongs_to :user association :$


2ae7dc2c71139b881b6fba1efda5a5ab

Question Chris - I'm trying to show the last 5 comments in the show view. I can't get the query string right. Thanks for your help!

Ce795239ba5dd2384fc2f88ffaff5451

You could change it to the following:

<% commentable.comments.order(created_at: :desc).limit(5).each do |comment| %>

2ae7dc2c71139b881b6fba1efda5a5ab

I should have mentioned that I'm showing the comments on an dashboard view that shows info from multiple models. So what you provided throws a 'undefined local variable or method `commentable' error.

For example I have a contacts model that is commentable and I would like to show the comments from that model on the dashboard(index)

Ce795239ba5dd2384fc2f88ffaff5451

Just ignore that part and add the order and limit functions to your call when you retrieve comments. That's the important bit there.

2ae7dc2c71139b881b6fba1efda5a5ab

That works.
I overlooked that their is a comments model to pull from.
Thanks!


5dfef8cab42377ad8165d40d0dd78f4b
Daniela Correa Orozco

Hi Chris!
Awesome video!
I'm having problems with current_user being nil in the base CommentsController where we set
@comment.user_id = current_user

current_user seems to return the right user_id in my other controllers. I have looked around but I haven't been able to pin point the problem (cookie problems maybe?)
Thank you in advance and for the really helpful videos!

Ce795239ba5dd2384fc2f88ffaff5451

Is the user not currently signed in by chance when you submit a comment?

5dfef8cab42377ad8165d40d0dd78f4b
Daniela Correa Orozco

yes, the user is signed in when I post a comment. I also have the before_action :authenticate_user! to make sure there is a logged in user available.

Ce795239ba5dd2384fc2f88ffaff5451

Hmm, I was hoping it wasn't. Usually it's just that the user isn't signed in. Otherwise...I'm not entirely sure. There aren't a whole lot of places to go check to make sure things are correct aside from that.

Possibly just a typo in your comment, but make sure you've got:

@comment.user_id = current_user.id

If you specify user_id, then you need to specify ID on the user, or you can just assign the object to the association alternatively:

@comment.user = current_user
5dfef8cab42377ad8165d40d0dd78f4b
Daniela Correa Orozco

That was it!

if I use
@comment.user = current_user I get a undefined method user so I changed it to .user_id but never specified ID of current user.

Thank you very much!

Ce795239ba5dd2384fc2f88ffaff5451

Awesome! You're welcome! :D


2ae7dc2c71139b881b6fba1efda5a5ab

I love this nifty bit of code. I'm wondering how would you go about using will_paginate to paginate the comments?


Acba3e7061eb78229a212161054be04b

I forgot about this. I watched it a year ago and used in in my last attempt at implementing these associations. And then I forgot about it. Im back on track again - thanks for this (again). I'm struggling to figure out how to filter the index of the comments resource by an attribute (say :status == 'published'). I can't do that in the regular way because the route is looking for a prefix (of film/actor)

Ce795239ba5dd2384fc2f88ffaff5451

Hey Melanie! :)

If you wanted a route to get all published comments (not ones scoped to a commentable type) you could add a resources :comments that was not nested in your routes and use that.

  resources :comments

resources :actors do
resources :comments, module: :actors
end
resources :films do
resource :comments, module: :films
end

And then you could make a comments_controller.rb that worked for all comments for any object. Is that what you're looking for?

Acba3e7061eb78229a212161054be04b

Hi Chris, I'm trying to make an index view (for comments) that shows all the comments on a specific film that are published. If comments wasn't a polymorphic resource, I could add published: true to the index path. But, since the comments view belongs to both actor and film I can't prefix the index with the parent name in the path. So I'm a bit stuck for what to do.

Ce795239ba5dd2384fc2f88ffaff5451

Ah, I gotcha. So in your films/comments_controller.rb you could say:

def index
@comments = @film.comments.where(status: 'published')
end

And then you would want to build the index.html.erb to display all those comments. Is that what you're looking for?

Acba3e7061eb78229a212161054be04b

Ab104ecf56b61af7295368ba3ee338eb

9db0054df76cc4b80888ff713527a652

578712174c45171ce625b844844ad5ae

Just coming back here to say thanks! I watched this several times and it eventually sunk in. I managed to set it up a few months ago and it's been working quite well. I will admit it did take me some time to grasp the concept though.

Ce795239ba5dd2384fc2f88ffaff5451

That's great to hear! :D And I agree, it's a tough one to wrap your head around the first time.


79a90653add7fbd41389676b6b053ec5

Hi all, i am getting issue with my routes when i follow this methodology. I get uninitialized constant Squeals.
SQUEAL Models

class Squeal < ActiveRecord::Base
has_many :comments, as: :commentable
end
comment.rb

class Comment < ActiveRecord::Base
belongs_to:commentable, polymorphic:true
end
/squeal/comments_controller.rb

class Squeals::CommentsController <commentscontroller before_action="" :set_commentable="" private="" def="" set_commentable="" @commentable="Squeal.find(params[:squeal_id])" end="" end="" comments="" controller="" class="" commentscontroller="" <="" applicationcontroller="" before_action:authenticate_user!="" def="" create="" @comment="@commentable.comments.new" comment_params="" @user.user="current_user" comment.save="" redirect_to="" @commentable,="" notice:="" "your="" comment="" was="" posted"="" end="" private="" def="" comment_params="" params.require(:comment).permit(:body)="" end="" end="" routes="" resources="" :squeals="" do="" resources="" :comments,="" module:="" :squeals="" end="">


F4d183246e7c3758ec90ad3cf6871124

Hi Chris, I want to add comments to multiple films at the same time. Suppose I want to add comments section on index page of films. Can you please help me how can i do that.


25ebfe133b5158232496d787a2fe0d66

thanks,It's really help full.If we want to delete the comment from actor or film we need to define a method as destroy or else using _destroy for nested attributes.Can anyone help me out.


Ee9d9d8ad2721937cf34caf569e85b01

@excid3:disqus
How would you need to modify this to work with deeply nested resources?

i.e:



resources :projects do
resources :project_users, path: :users, module: :projects
resources :posts do
resources :comments, module: :posts
end
end


Bdf234c0e938e25158530f60e29b93e8

I am trying to use commentable with actioncable , but I keep getting the following error
d6f951d17) from Async(default) in 20.73ms: ActionView::Template::Error (undefined method `comments' for #<class:0x007f5cec08b880>):
someone advice me to look for value of comments and I realized is not define
but my question is Is it necessary to define relationship between comment and lets say article class ?

class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true #, optional: true
belongs_to :article, optional: true <--- this point

validates :body, presence: true, length: {minimum: 5, maximimum: 1000 }
after_create_commit {CommentBroadcastJob.perform_later self}
end


Fecef236fdd08050b739cef24565c947
Being new at programming and Rails this is the first time I land on the polymorphic association concept and got It completely. I find It super useful and You made It "easy" to understand and implement. Thanks!

4feb8bf1e6aae4531c7ec3e441d216db

Hi Chris! Can this form be used on an index page? I'm noticing that I only get it to work on a show page. Any help


Fab02c9dc32d2fde05fa35f05b60eaaa

It would great if you could expand this to include comment replies. There are no great tutorials on how to accomplish this task.

Ce795239ba5dd2384fc2f88ffaff5451

Been planning on doing that soon. Thinking about doing this in a series where we create an embeddable Javascript comment system like Disqus.

C804b6d0315e6332e231295349f90485

I'd love to see this as well. Tried briefly and was unsuccessful.


Fab02c9dc32d2fde05fa35f05b60eaaa

That would be awesome. Thanks!


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 18,000+ developers who get early access to new screencasts, articles, guides, updates, and more.

By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

More of a social being? We're also on Twitter and YouTube.