154: Polymorphic Association
Polymorphic associations aren’t a new feature of Rails, they’ve been around since Rails 1.1, but they can be a little confusing for less experienced Rails developers to understand. In this episode we’re going to give a simple demonstration of how to use them that will hopefully explain how they’re used.
Above is a page from a site that has three different models: articles, photos and events. We’d like to enhance the site so that its users can add comments to an article, a photograph or an event. If we were just adding comments to one of the models, say articles we’d create a
Comment model and have a
belongs_to relationship between
Comment. Without polymorphic associations we’d have to create three different types of comment model so that each one could
belong_to the appropriate model, which would create a lot of repetition in the code. Polymorphic associations allow us to create just one Comment model and have each comment know which other model it should be associated with.
Creating The Comment Model
The first thing to do is to create the
Comment model. We’ll create this in the normal way, but with one small difference. If we were just creating comments for an
Article we’d have an integer field called
article_id in the model to store the foreign key, but in this case we’re going to need something more abstract. All of the models we’re adding comments to have one thing in common: they allow comments. Therefore we’ll call the foreign key
commentable_id. A comment will need to know which model it’s associated with, so we’ll need another field to store that, which we’ll call
commentable_type. This field will hold the class name of the model the comment is associated with.
script/generate model Comment content:text commentable_id:integer commentable_type:string
Once we’ve created the model and run the migration we’ll need to associate our comment model with the article, photo and event models.
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end
Instead of saying that a comment
belongs_to another model, we make it belong to
commentable and declare it as a polymorphic relationship. Now, any other model that
has_many commentables can have comments. In the
Event classes we define the relationship like this.
has_many :comments, :as => :commentable
This tells the classes that they have many comments through the polymorphic commentable relationship.
Sorting Out Comments and Views
Now that the relationships are set up between our models we need to look at the controllers and views. We’ll need a controller to go with our
script/generate controller Comments
We can mostly treat our comments model as we would any other relationship, for example to get an article’s comments we can use
Getting The Right Comments
Difficulty can arise when we try to use nested resources, such as
to see all of the comments for an article. To get this to work we need to configure the
routes.rb file so that the comments are seen as a nested resource for each of the other models.
map.resources :articles, :has_many => :comments map.resources :photos, :has_many => :comments map.resources :events, :has_many => :comments
If we go to URL that shows the comments for a given article (
index action in the
CommentsController will be executed, and here we find a problem.
def index @comments = Comment.all end
The same index action will be called whether we get the comments for an
Photo or an
Event. We need a way for the
index action to know which comments to return. One way to do this is to loop through the parameters passed to the action to look one called
<parent_resource>_id which will enable us to know which of the
commentable models we’re dealing with. We can write a method to do this and add it to the
def find_commentable params.each do |name, value| if name =~ /(.+)_id$/ return $1.classify.constantize.find(value) end end nil end
The method above loops through each of the params and looks for one ending in
_id. If we were looking for the first
Article’s comments then there should be a parameter called
article_id with a value of
If the method finds a matching parameter it calls
classify on the part of the name before the
_id to turn in it from a table name in to a model name (so
“article” will become
“Article”) and then calls
constantize on that which tries to find a constant to match the name in the string returned by
find is called on that constant with the value of the matching key to return the
commentable record whose comments we’re looking for. In our example it will return the first
We can now use this method in the index action to get the appropriate comments.
def index @commentable = find_commentable @comments = @commentable.comments end
Adding a Comment
As well as listing the comments on the
index page we want users to be able to add a new comment. Here’s what the view code looks like.
<h1>Comments</h1> <ul id="comments"> <% @comments.each do |comment| %> <li><%= comment.content %></li> <% end %> </ul> <h2>New Comment</h2> <% form_for [@commentable, Comment.new] do |form| %> <ol class="formList"> <li> <%= form.label :content %> <%= form.text_area :content, :rows => 5 %> </li> <li><%= submit_tag "Add comment" %></li> </ol> <% end %>
The first part of the view code renders the comments as an unordered list. Below that is the form for adding a comment. As the form is for a nested resource we pass an array to
form_for; the first element in the array being the
commentable object we returned from
find_commentable and the second being a new
When the form is submitted it will call the
create action of the
def create @commentable = find_commentable @comment = @commentable.comments.build(params[:comment]) if @comment.save flash[:notice] = "Successfully saved comment." redirect_to :id => nil else render :action => 'new' end end
The first thing to note here is that we use
find_commentable again to get the current commentable record, be it an
Event. Once we’ve got it we use
build to create the new
Comment. Once we’ve saved the model we have a problem knowing which
index page to redirect back to as we don’t know what resource the comment is nested under. We can take advantage of a small hack and redirect to an
nil. That will redirect back to the current index action and show the right page.
Our comment form works and redirects back to the index action.
Our comment form now works. We’ve added a comment and it has redirected back to the correct page. This will work too with comments for the
An Exercise For The Reader
The next thing we’d have to do would be to apply what we’ve done to the
create actions to the other actions in the
CommentsController. That will be left as an exercise for the reader.