Posts Tagged ‘polymorphism’

Nested Comments in Ruby on Rails, part 2: Controllers and Views

January 26, 2011

  1. The Model Layer
  2. Controllers and Views
 

Part 1 of this series came out exactly 3 months and 3 days ago. Special thanks to a reader named Edward who prodded me to finally add the controllers and views to this.

Going beyond the model layer for nested comments introduces a new programming idiom: recursion. Some ruby developers may not be familiar with it – especially if your experience is mostly web-related, where the need doesn’t come up as often. Recursion in a nutshell is the act of a method calling itself. If you’ve seen Inception, The ability to have dreams within dreams within dreams means those dreams are recursive. If you haven’t seen the movie, think of russian matryoshka dolls. You won’t experience star-studded special effects with the dolls, but you’ll at least get the idea of recursion.

Unlike russian dolls or most of Leo’s recent work, recursion in software is potentially infinite. Practically speaking though, it’s more like the doll thing. After all, a system only has so many resources, and recursion is expensive in this regard – the method must copy itself in memory at each layer, local variables and all. On the plus side, they tend to be lightning fast compared to standard iteration using loops. And in our case, we’ll be hitting the database at each layer. We’ll ignore the dangers in our simple app, though.

Routing

Let’s start with our routing file:

# config/routes.rb
NestedComments::Application.routes.draw do
  resources :comments do
    resources :comments
  end

  resources :posts do
    resources :comments
  end
  
  root :to => 'posts#index'
end

Working backward, we’re making our Posts controller’s index action our default route. That’s just to get the app functional. Next comes something interesting: nesting our comments inside of our posts. Interesting, but boring. Finally, the main event: nesting our comments within our comments!

Before you get too excited and start pulling out your Nana’s childhood russian doll set for comparision, this isn’t true recursion. It’s well documented that nesting resources any more than two layers deep is painful and unnecessary, so think of this as the lamest russian doll ever.

Controllers

First, our Posts controller, which is less exciting:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])
  end

  def new
    @post = Post.new
  end
  
  def create
    @post = Post.new(params[:post])
    
    if @post.save
      redirect_to posts_path, :notice => "Your post was created successfully."
    else
      render :action => :new
    end
  end
end

We’re setting up a pretty standard restful resource here, with a couple actions skipped for simplicity. Now the comments controller (get those dolls ready):

# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_filter :get_parent
  
  def new
    @comment = @parent.comments.build
  end

  def create
    @comment = @parent.comments.build(params[:comment])
    
    if @comment.save
      redirect_to post_path(@comment.post), :notice => 'Thank you for your comment!'
    else
      render :new
    end
  end

  protected
  
  def get_parent
    @parent = Post.find_by_id(params[:post_id]) if params[:post_id]
    @parent = Comment.find_by_id(params[:comment_id]) if params[:comment_id]
    
    redirect_to root_path unless defined?(@parent)
  end
end

It’s not much bigger, but there’s a lot going on here! First, since comments are nested, we have to look for a parent. We’re only creating comments in this example, so we only have those related actions. Comments will always be shown on a post page.

The really exciting part is after a successful comment creation. How do we redirect back to the post page? For all we know, this comment could buried down 12 layers of replies. All we really have access to so far is the parent of the object. This necessitates a new model method:

# exerpt from app/models/comment.rb
def post
  return @post if defined?(@post)
  @post = commentable.is_a?(Post) ? commentable : commentable.post
end

Recursive functions are often short and sweet for two reasons: they’re already complex by nature, and adding more code than necessary would make them unmanageable. Also, they’re getting a lot done in just a few lines. In this case, the second line is the key: if “commentable” (the parent object) is a post, return that. Otherwise, call this same method on the parent, which will in turn check if *it* is a Post, and so on.

I could have written it shorter, like this:

def post
  commentable.is_a?(Post) ? commentable : commentable.post
end

In fact, I did at first. But the extra code that checks and sets an instance variable is caching the result. This way, if we call the same method on an object more than once, it stores the result for future use. Remember, recursion can be expensive – especially when the database is involved.

Views

Finally, it’s view time, with one more bit of recursion for fun.

Or post views are standard scaffolding mostly, with the exception of the show view:

# app/views/posts/show.html.erb
<h1><%= @post.title %></h1>

<div class="body">
  <%= @post.body %>  
</div>

<h2>Comments</h2>

<p><%= link_to 'Add a Comment', new_post_comment_path(@post) %></p>

<ul class="comment_list">
  <%= render :partial => 'comments/comment', :collection => @post.comments %>
</ul>

Notice we have the partial app/views/comments/_comment.html.erb. We’re calling this for each of our post’s comments. Nothing too fancy here. Now, for the partial itself:

# app/views/comments/_comment.html.erb
<li class="comment">
  <h3><%= comment.title %></h3>

  <div class="body">
    <%= comment.body %>
  </div>
  
  <p><%= link_to 'Add a Reply', new_comment_comment_path(comment) %></p>
  
  <% unless comment.comments.empty? %>
    <ul class="comment_list">
      <%= render :partial => 'comments/comment', :collection => comment.comments %>
    </ul>
  <% end %>
</li>

This partial is recursive! The comments controller doesn’t have a show method, because we’re never going to view a comment by itself. Instead, the show-like code is in this partial, and at the end it checks to see if *this* comment has comments. If so, it calls the partial again on the whole collection. The end result is a nested, bulleted list of comments. This is not very sexy if you fire up the code yourself, but it’s a great starting point.

Summary

Hopefully this article as done a good job of explaining both recursion, and how to use it to achieve nested comments in your applications. If you’re new to recursion as a concept, haven’t seen Inception, didn’t inherit russian dolls from Nana or receive them as a snazzy graduation present, and my explanation somehow fell short, it’s a well documented programming idiom. There are tons of resources online, so take the time to learn this powerful tool, then learn not to overuse it :)

Please download the code and play with it if you want to learn more – the code is fully test-driven so you can see how that works, which is just as important.

On a final note, I’m tempted to do a follow-up article with ajax and some nicer formatting. Perhaps in 3 months and 3 days…

Nested Comments in Ruby on Rails, Part 1: Models

October 23, 2010

  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

      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:

  rake db:migrate

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.

Common Addresses Using Polymorphism and Nested Attributes in Rails

October 19, 2010

Have you ever wanted to take an object that is common to a lot of models – like addresses – and DRY up your code? If you’re really concerned about design, or if the object itself is complex, you certainly want to make changes in one place and have them apply to everywhere in your app.

You can use a combination of polymorphism, nested attributes, and shared views to accomplish this easily. If it seems complicated at first, try it a couple times and you’ll see it’s no big deal. Let’s get started.

Polymorphism

This is the concept that something like an address can belong to more than one type of thing. Maybe your app has customers, employees, and locations, which all need addresses and you’d like them to appear uniform from one form to the next. Let’s create a migration and model for a simple address.

In your migration, we need the address fields you plan to use, plus the two fields that make polymorphism possible in rails: `object_id` and `object_type`. This is how rails will know what object type and id each address belongs to:

    create_table :addresses do |t|
      t.string :line1
      t.string :line2
      t.string :city
      t.string :state
      t.string :zip
      t.integer :addressable_id
      t.string :addressable_type

      t.timestamps
    end
 
    add_index :addresses, [:addressable_type, :addressable_id], :unique => true

In your model, we need to tell `address` that is belongs to other things polymorphically:

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

Nested Attributes

Now let’s make the customer model “contain” an address. In addition to the `has_one` association, we’re going to tell rails that customer forms might also contain fields for the customer’s address as well:

class Customer < ActiveRecord::Base
  has_one :address, :as => :addressable
  accepts_nested_attributes_for :address
end

Shared Views

Next, we’ll want to make a partial to store the address form, that can be called from our other views. Put this in `app/views/shared/_address.html.erb`:

<p>
  <%= f.label :line1, 'Address 1' %><br />
  <%= f.text_field :line1 %>
</p>

<p>
  <%= f.label :line2, 'Address 2' %><br />
  <%= f.text_field :line2 %>
</p>

<p>
  <%= f.label :city %><br />
  <%= f.text_field :city %>
</p>

<p>
  <%= f.label :state %><br />
  <%= f.text_field :state, :size => 2 %>
</p>

<p>
  <%= f.label :zip, 'Zip Code' %><br />
  <%= f.text_field :zip %>
</p>

Where does the `f` come from in the view above? It’s passed in by any view that wants to use it. Let’s use the new customer form as an example:

<% form_for(@customer) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  
  <% f.fields_for :address do |address| %>
    <%= render :partial => 'shared/address', :locals => {:f => address} %>
  <% end %>
  
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

The `fields_for` call above is the secret sauce, calling our shared partial and passing the form object `address` into the partial as the variable `f`. When you view the new customer page, you’ll see the address fields included as if they were part of the customer form directly. The real power is that you can repeat these steps for all other models that have an address, and they’ll use the same database table, model, and shared view for addresses.

…And Other Junk

There’s one last step. In the `new` action of your `customers` controller, you need to build the address object onto the customer object, so rails knows it’s there:

  def new
    @customer = Customer.new
    @customer.build_address
  end

That’s all it takes to get polymorphism, nested attributes, and shared views working together to DRY up your code, data modeling, and views. And DRYer code equals happier coders! For more info on polymorphism and nested attributes, visit your friendly neighborhood documentation:

http://wiki.rubyonrails.org/howtos/db-relationships/polymorphic
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html


Follow

Get every new post delivered to your Inbox.