In our recurring Time and Expenses example, let’s assume that we want both BillableWeek and Timesheet to have many comments (a shared Comment class). A naive way to solve this problem might be to have the Comment class belong to both the BillableWeek and Timesheet classes and have billable_week_id and timesheet_id as columns in its database table.
class Comment < ActiveRecord::Base belongs_to :timesheet belongs_to :expense_report end
I call that approach is naive because it would be difficult to work with and hard to extend. Among other things, you would need to add code to the application to ensure that a Comment never belonged to both a BillableWeek and a Timesheet at the same time. The code to figure out what a given comment is attached to would be cumbersome to write. Even worse, every time you want to be able to add comments to another type of class, you’d have to add another nullable foreign key column to the comments table.
Rails solves this problem in an elegant fashion, by allowing us to define what it terms polymorphic associations, which we covered when we described the :polymorphic => true option of the belongs_to association in Chapter 7, Active Record Associations.
Using a polymorphic association, we need define only a single belongs_to and add a pair of related columns to the underlying database table. From that moment on, any class in our system can have comments attached to it (which would make it commentable), without needing to alter the database schema or the Comment model itself.
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end
There isn’t a Commentable class (or module) in our application. We named the association :commentable because it accurately describes the interface of objects that will be associated in this way. The name :commentable will turn up again on the other side of the association:
class Timesheet < ActiveRecord::Base has_many :comments, :as => :commentable end class BillableWeek < ActiveRecord::Base has_many :comments, :as => :commentable end
Here we have the friendly has_many association using the :as option. The :as marks this association as polymorphic, and specifies which interface we are using on the other side of the association. While we’re on the subject, the other end of a polymorphic belongs_to can be either a has_many or a has_one and work identically.
Here’s a migration that will create the comments table:
class CreateComments < ActiveRecord::Migration def self.up create_table :comments do |t| t.text :body t.integer :commentable t.string :commentable_type end end end
As you can see, there is a column called commentable_type, which stores the class name of associated object. The Migrations API actually gives you a one-line shortcut with the references method, which takes a polymorphic option:
create_table :comments do |t| t.text :body t.references :commentable, :polymorphic => true end
We can see how it comes together using the Rails console (some lines ommitted for brevity):
>> c = Comment.create(:text => "I could be commenting anything.") >> t = TimeSheet.create >> b = BillableWeek.create >> c.update_attribute(:commentable, t) => true >> "#{c.commentable_type}: #{c.commentable_id}" => "Timesheet: 1" >> c.update_attribute(:commentable, b) => true >> "#{c.commentable_type}: #{c.commentable_id}" => "BillableWeek: 1"
As you can tell, both the Timesheet and the BillableWeek that we played with in the console had the same id (1). Thanks to the commentable_type attribute, stored as a string, Rails can figure out which is the correct related object.
There are some logical limitations that come into play with polymorphic associations. For instance, since it is impossible for Rails to know the tables necessary to join through a polymorphic association, the following hypothetical code, which tries to find everything that the user has commented on, will not work.
class Comment < ActiveRecord::Base belongs_to :user # author of the comment belongs_to :commentable, :polymorphic => true end class User < ActiveRecord::Base has_many :comments has_many :commentables, :through => :comments end >> User.first.comments ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have a has_many :through association 'User#commentables' on the polymorphic object 'Comment#commentable'.
If you really need it, has_many :through is possible with polymorphic associations, but only by specifying exactly what type of polymorphic associations you want. To do so, you must use the :source_type option. In most cases, you will also need to use the :source option, since the association name will not match the interface name used for the polymorphic association:
class User < ActiveRecord::Base has_many :comments has_many :commented_timesheets, :through => :comments, :source => :commentable, :source_type => 'Timesheet' has_many :commented_billable_weeks, :through => :comments, :source => :commentable, :source_type => 'BillableWeek' end
It’s verbose, and the whole scheme loses its elegance if you go this route, but it works:
>> User.first.commented_timesheets => [#<Timesheet ...>]