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 t.timestamps end add_index :comments, [:commentable_id, :commentable_type] end
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:
Now let’s setup our associations in our models, starting with posts:
class Post < ActiveRecord::Base has_many :comments, :as => :commentable end
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 end
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.