Recipe 3.21. Factoring Out Common Relationships with Polymorphic AssociationsProblemContributed 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. SolutionPolymorphic 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={}>> DiscussionPolymorphic 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 See Also
|