Factoring Out Common Relationships with Polymorphic Associations

Problem

Contributed by: Diego Scataglini

When modeling entities in your application, it's common for some of them to exhibit the same relationships. For example, a person and a company may both have many phone numbers. You'd like to design your application in a flexible way that lets you add many models with the same relationship while keeping your database schema clean.

Solution

Polymorphic associations offer a simple and elegant solution to this problem. For this recipe, assume you've got an empty Rails application and have configured your database settings. Begin by generating a few models:

$ ruby script/generate model Person
$ ruby script/generate model Company
$ ruby script/generate model PhoneNumber

Next, add some relationships between the models. These relationships will be polymorphic, that is they will share a generic name that represents the role they play in the relationship.

For instance, since you can call both companies and individuals using a phone number, you'll refer to them as "callable." You can also use "dialable" or "party"; the name you choose is mostly a matter of personal preference and readability. Specify the relationships among the models as shown:

app/models/company.rb:

class Company < ActiveRecord::Base
 has_many :phone_numbers, :as => :callable, :dependent => :destroy end

app/models/person.rb:

class Person < ActiveRecord::Base
 has_many :phone_numbers, :as => :callable, :dependent => :destroy end

app/models/phone_number.rb:

class PhoneNumber < ActiveRecord::Base
 belongs_to :callable, :polymorphic => :true end

The :as option above specifies the generic name you'll use to refer to the Company and Person classes. This name should match the symbol passed to belongs_to. Notice the :polymorphic => true, which is the key to making polymorphic associations work.

First, define the table structures in your migration files, and create some test data:

db/migrate/001_create_people.rb:

class CreatePeople < ActiveRecord::Migration
 def self.up
 create_table :people do |t|
 t.column :name, :string
 end
 Person.create(:name => "John Doe")
 end
 def self.down
 drop_table :people
 end end

db/migrate/002_create_companies.rb:

class CreateCompanies < ActiveRecord::Migration
 def self.up
 create_table :companies do |t|
 t.column :name, :string
 end
 Company.create(:name => "Ruby Bell")
 end
 def self.down
 drop_table :companies
 end end

There are two fields in the phone_numbers table that enable Rails to work its magic. These are callable_id and callable_type. Rails will use the value of the callable_type field to figure out which table to query (and which class to instantiate). The callable_id field specifies the matching record. Here's a migration for this table:

db/migrate/003_create_phone_numbers.rb:

class CreatePhoneNumbers < ActiveRecord::Migration
 def self.up
 create_table :phone_numbers do |t|
 t.column :callable_id, :integer
 t.column :callable_type, :string
 t.column :number, :string
 t.column :location, :string
 end
 end
 def self.down
 drop_table :phone_numbers
 end end

Run the migrations:

$ rake db:migrate

Now, with everything set up and ready to go, you can inspect your application in the Rails console:

$ ruby script/console -s
Loading development environment in sandbox.
Any modifications you make will be rolled back on exit.
>> person = Person.find(1)
=> #<Person:0x37072ec @attributes={"name"=>"John doe", "id"=>"1"}>
>> person.phone_numbers
=> []
>> person.phone_numbers.create(:number => "954-555-1212", :type => "fake")
=> #<PhoneNumber:0x36ea3b8 @attributes={"callable_type"=>"Person", 
"number"=>"954-555-1212", "id"=>1, "callable_id"=>1, "location"=>nil}, 
@new_record=false, @errors=#<ActiveRecord::Errors:0x36e7b2c 
@base=#<PhoneNumber:0x36ea3b8 ...>, @error s={}>>
>> person.reload
>> person.phone_numbers
=> [#<PhoneNumber:0x36d8bcc @attributes={"callable_type"=>"Person", 
"number"=>"954-555-1212", "id"=>"1", "callable_id"=>"1", 
"location"=>nil}>]
> #as expected it works equally well for the Company Class
>> number = Company.find(1).create_in_phone_numbers(
?> :number => "123-555-1212",:type => "Fake office line")
=> #<PhoneNumber:0x3774108 @attributes={"callable_type"=>"Company", 
"number"=>"123-555-1212", "id"=>2, "callable_id"=>1, "location"=>nil}, 
@new_record=false, @errors=#<ActiveRecord::Errors:0x37738fc 
@base=#<PhoneNumber:0x3774108 ...>, @errors={}>>

Discussion

Polymorphic associations are a powerful tool for defining one-to-many relationships. A polymorphic association defines a common interface that sets up the relationship. By convention, the interface is represented by an adjective that describes the relationship (callable in this solution). Models declare that they adhere to the interface by using the :as option of the has_many call. Thus, in the solution, the Person and Company models declare that they are callable. Active Record gives these classes the necessary accessor methods to work with phone numbers.

For this type of association to work, you need to add two fields to the table representing the polymorphic model. These two fields are required to be named

<interface
 name>
_id and
<interface
 name>
_type. They store the primary row ID and class name of the object to which the association refers.

See Also