Antonio Antonio - 23 days ago 7
Ruby Question

Rails 5: Best way to iterate through a polymorphic model and find the top level parent?

I'm trying to implement a "most recent comments" for a user in their control panel. A comment can be left on a

Post
or another
comment
.

In my view I want to display the
:body
of the
comment
along with the
post
it was created in via a hash. That goes for a
comment
s
comment
as well. Luckily, Comment's can only go two deep like so

Post
Comment
Child Comment # Would like to find the parent post.
Child Comment
Comment


I had a method, which I will input below, which worked well for
comment
s that were left on a post, but would break when trying to find a
comment
had a
commentable_type: "Comment"
.

My only solution would be to have an
if
statement that said
if commentable_type == "Comment"
, find that Comments parent, grab the Post, then take the Post` and bring it back down to that child comment and push it into the hash that way.

I was wondering if there is a more efficient way (either AR or SQL) to do such a thing so that in the end, I can find the
comment
's parent
Post
is in even if it's a
comment
of a
comment
.

Here is all the information that I think will help:

Schema:

create_table "comments", force: :cascade do |t|
t.text "body"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "commentable_id"
t.string "commentable_type"
t.integer "user_id"
end

create_table "forums", force: :cascade do |t|
t.string "name"
t.text "description"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_forums_on_user_id", using: :btree
end

create_table "posts", force: :cascade do |t|
t.string "title"
t.text "description"
t.integer "user_id"
t.integer "forum_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["forum_id"], name: "index_posts_on_forum_id", using: :btree
t.index ["user_id"], name: "index_posts_on_user_id", using: :btree
end

...User table omitted for brevity...


Models:

#post model
belongs_to :user
belongs_to :forum

has_many :comments, as: :commentable

#comment model
belongs_to :user
belongs_to :commentable, polymorphic: true
has_many :comments, as: :commentable


Method as it stands now. Wont work when it finds a comment with
commentable_type: "Comment"
:

def self.find_all_user_comments(user)
@comment_hash = []
@user_comments = Comment.where(user_id: user.id)
@user_comments.all.each do |co|
post = Post.find(co.commentable_id)
@comment_hash.push( { comment: co, post: post} )
end
@comment_hash
end


View:

<div class="col-md-10 col-md-offset-1">
<h3>Recent comments:</h3>
<% if @comments.any? %>
<table class="table">
<thead>
<tr>
<th>Comment</th>
<th>Post</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @comments.each do |co| %>
<tr class="comment-<%= co[:comment].id %>">
<td><%= truncate(co[:comment].body, length: 50) %></td>
<td><%= truncate(co[:post].title, length: 50) %></td>
<td><%= link_to "destroy", comment_path(co[:comment]), remote: true, method: :delete, data: {confirm: "Are you sure you want to delete this comment?"} %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>

Answer

You could add a method to your comment class, and use a bit of recursion to get the job done (well not technically true recursion as it calls the method on a second instance, but we can use a similar concept), something along the lines of:

def find_parent_post
    return self.commentable if self.commentable.is_a?(Post)
    self.commentable.find_parent_post
end

This will also be future proof for if you ever want to nest your comments even deeper.