Skip to main content

Comments With Polymorphic Associations Discussion

General • Asked by Chris Oliver

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


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

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

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.

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

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

+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. 

Polymorphism is the main anti-pattern in rails(


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


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?

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.


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


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.

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.


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

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


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


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!

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!!

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

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.

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

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.


How do you add a destroy method to this


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!

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.

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?

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.

Thanks so much Chris.


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

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?

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

Do you have the part that sets @article?

Hi - what part would that be?
def set_article

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

authorize @article

end

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.

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'

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!

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

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?

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).

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.

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]

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)

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.

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.

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).

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

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. :)

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.

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.


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!


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?

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.


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?

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


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!

You could change it to the following:

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

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)

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

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


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!

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

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.

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
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!

Awesome! You're welcome! :D


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


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)

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?

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.

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?




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.

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


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="">


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.


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.


@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


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


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!

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


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

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

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


That would be awesome. Thanks!


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 27,623+ 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.