Recipe 3.21. Factoring Out Common Relationships with Polymorphic Associations


Recipe 3.21. 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

  • Section 3.22"




Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net