rails has_many on polymorphic


9.7.1. In the Case of Models with Comments

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.

The Interface

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.

The Database Columns

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.

has_many :through and Polymorphics

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 ...>]

你可能感兴趣的:(rails has_many on polymorphic)