Nested Comments in Ruby on Rails, Part 1: Models

  1. The Model Layer
  2. Controllers and Views

YouTube has a pretty cool comment system. You can comment on videos, but you can also reply to comments other people have posted. In essence, you commenting on comments!

If you’d like something similar in your app, you might be tempted to create PostComments and CommentComments, or something similar. A better approach is to use polymorphic associations. Polymorphism makes it possible for a comment to belong to a post, or another comment, or any number of things. And it’s easier than you think.

Note: all examples will be in Rails 3. If you’re not familiar, that’s okay. The example code is available from github, in my examples of nested comments.

Let’s start by creating a Post model:

  rails g model post title:string body:text
  rake db:migrate

We’re keeping it simple, and we’re not going to bother with user authentication for these examples. Our post model is pretty straightforward, so we didn’t need to modify the migration file at all. Now let’s create our comments:

  rails g model comment title:string body:text commentable_id:integer commentable_type:string

This is where some of the magic comes in. We can’t say our comments “belong to posts” because they can belong to anything. So we can’t add a “post_id” field. Instead, we come up with a name for our association: “commentable”. We add a commentable_id field to store the id of the object this comment belongs to. And we add a commentable_type field to store the type of object this comment belongs to – ‘Post’, ‘Comment’, whatever. With these two pieces of info, Rails can figure out the rest and make your life easier.

Before we can add comments to our database, however, we need to make a small change the code in the “self.up” part of our migration:

  def self.up
    create_table :comments do |t|
      t.string :title
      t.string :body
      t.integer :commentable_id
      t.string :commentable_type

    add_index :comments, [:commentable_id, :commentable_type]

We’ve added an single index on the combination of the commentable fields, which will speed up our app. Now we can migrate the changes again:

  rake db:migrate

Now let’s setup our associations in our models, starting with posts:

  class Post < ActiveRecord::Base
    has_many :comments, :as => :commentable

Normally if you tell Post that it has_many comments, Post would expect the comments table to have a “post_id” field. It doesn’t, because we’re using polymorphism. So we tell it the name we gave our polymorphic association: “commentable”.

Now for the slightly more complicated comment model:

  class Comment < ActiveRecord::Base
    belongs_to :commentable, :polymorphic => true
    has_many :comments, :as => :commentable

First, we’re setting up the “belongs to” side of polymorphism. Our comment belongs to “commentable”, the name we gave our polymorphic association. We also tell it that this is polymorphic. Otherwise, Rails would look for a model called Commentable.

Finally, we’re saying that comments have many comments, the same way set did it for posts.

You might thing that after this special setup, using polymorphic associations would be more difficult as well. The good news is, the hard part is over and everything else works the same as any other “belongs_to” or “has_many” association. Let’s go into the rails console to try it out:

  post = Post.create :title => 'First Post'
  => #<Post id: 1, title: "First Post", body: nil, created_at: "2010-10-23 16:56:13", updated_at: "2010-10-23 16:56:13"> 

  comment = post.comments.create :title => 'First Comment'
  => #<Comment id: 1, title: "First Comment", body: nil, commentable_id: 1, commentable_type: "Post", created_at: "2010-10-23 16:56:40", updated_at: "2010-10-23 16:56:40"> 

  reply = comment.comments.create :title => 'First Reply'
  => #<Comment id: 2, title: "First Reply", body: nil, commentable_id: 1, commentable_type: "Comment", created_at: "2010-10-23 16:59:28", updated_at: "2010-10-23 16:59:28"> 

We’re able to add a comment to our post, and add a comment to that comment! Rails does the work of filling in the commentable_id and commentable_type fields, just as it would have filled in the post_id field if comments could only belong to posts.

Please check out the nested comments example code on github, which includes tests. Download it play around with it, and see how it works. In the next part, I’ll be looking at how to use nested comments in your controllers and views.


Tags: , , , , , ,

11 Responses to “Nested Comments in Ruby on Rails, Part 1: Models”

  1. Joseph Says:

    Thanks! This is really helpful and I’m looking forward to ‘part 2’ since I’m having a more difficult time tackling the subsequent forms.

    • Jaime Bellmyer Says:

      Thanks, and this is just the shove I need to get the other few parts done. Controllers are next, followed by ajax, and then a closer look at the test-driven development that was used to create it all. I should have this done by the weekend.

  2. Eric Says:

    Considering this approach, but aren’t there issues in efficiency with pulling descendant (children and children of children) comments? I’m reconsidering something like acts_as_commentable_with_threading.

    • Jaime Bellmyer Says:

      There can be efficiency issues, yes – certainly over a single-tiered comment solution. But this article is about allowing nested comments. There are a few ways to speed this up: including nested associations, for one. You could also potentially limit nesting to a certain number of levels, and reflect that in your includes.

      I also meant this more as a post to demonstrate how to use polymorphism – comments are just a simple example, and of course there are plugins/gems galore that solve this problem in many different ways. Thanks for the heads up on acts_as_commentable_with_threading, that sounds like it will be worth checking out.

  3. Vladimir Says:

    Great post! I looking forward to second part.

    I have one question:

    What if we have big number of nested comments? Very-very big tree of comments with few hundreds of comments. Each comment = new DB query for searching all nested comments. That maybe high load to DB server… What we can do with high load? What are alternative ways you know for creating nested comments?

  4. Вложенные комментарии в Ruby on Rails: Модели. | Разработка на Ruby и Rails c нуля Says:

    […] Оригинал статьи на английском: Nested Comments in Ruby on Rails, Part 1: Models […]

  5. Rommel Says:

    This is a truly nice post. I can’t wait to see the other one. I’m also using Ruby on Rails on most of my apps and to help me with the jobs, I use the Background Job Rails.

  6. none Says:

    Perhaps it will be a stupid question but where table commentable?
    Or Rails will create its own?

  7. Jaime Bellmyer Says:

    That’s actually a very good question, and it’s confusing the first time you see polymorphism in Ruby on Rails. There is no commentable table, because Rails has everything it needs to connect comments with their owners.

    If comments only belonged to one thing, like posts, the comments table would have a foreign key post_id. Instead, since comments can belong to any other model, the table has two keys: commentable_id and commentable_type. The type column holds the class name of the model it belongs to!

    Now when you type comments.commentable (to find the object that the comment belongs to) it finds the correct owner model in commentable_type, and searches that model for the id found in commentable_id.

    But what if you want to go the other way, and find all the comments that belong to a post? Rails (ActiveRecord, to be specific) looks for the comments that have comment_type “Post”, and a comment_id that matches the post in question.

    I hope this helps!

  8. Bryan Jacobson (@BryanJacobson6) Says:

    This was so helpful! Thank you so much!

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: