Skip to content

Instantly share code, notes, and snippets.

@jcasimir
Created July 21, 2011 04:07
Show Gist options
  • Select an option

  • Save jcasimir/1096469 to your computer and use it in GitHub Desktop.

Select an option

Save jcasimir/1096469 to your computer and use it in GitHub Desktop.
Polymorphism

Polymorphism

Sometimes relationships need to be flexible, and that's where we look to polymorphism. Say we want to implement:

  • A Person
  • A Company
  • A PhoneNumber that can connect to a Person or a Company

At the Database Level

A naive implementation would be to add both person_id and company_id columns to the phone_numbers table. Then in the model:

class PhoneNumber < ActiveRecord::Base
  belongs_to :person
  belongs_to :company
end

This is wrong because it implies that a single PhoneNumber can connect to both a Person and a Company. Furthermore, as you add more contact types down the road, you'll have to keep adding columns to phone_numbers.

Setup for Polymorphism

Instead, Rails' implementation of polymorphism relies on a two-factor foreign key. Instead of just having a single something_id column pointing to the external record, we'll use a something_id and something_type to record the id number and the class name of the foreign object.

In this domain, contact would be an adequate generalization of Person and Company. Our phone_numbers table should then have columns contact_id and contact_type. The records will then look like this:

-------------------------------------------
|id| number       |contact_id|contact_type|
| 1| "2223334444" | 2        | "Person"   |
| 2| "5554443333" | 3        | "Person"   |
| 3| "6667774444" | 3        | "Company"  |
-------------------------------------------

Note that the contact_id values won't be unique. Phone numbers with IDs 2 and 3 can share the same contact_id and have different contact_types, thus connecting them to different objects.

In the Rails Models

With the data tables setup, we need to tell Rails how to understand these relationships.

Implementation using One-to-One

First, let's consider a one-to-one connection that limits a Person or Business to having just one PhoneNumber.

Looking just at the Person, we'd normally write:

class Person < ActiveRecord::Base
  has_one :phone_number
end

But that won't work because Rails will expect to find a person_id column in phone_numbers. We tell it to look, instead, for named polymorphic columns:

class Person < ActiveRecord::Base
  has_one :phone_number, :as => :contact
end

Rails will then anticipate phone_numbers to have contact_id and contact_type. The implementation in Company would be exactly the same.

In PhoneNumber, we tell it about the relationship to contacts:

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

Usage

Assuming we have an instances of these classes like @phone_number, @person, or @company:

@company.phone_number
@person.phone_number
@phone_number.contact

For the first two, there's no apparent difference between the polymorphic usage and a normal has-one. That's the beauty of it!

For @phone_number.contact, you'll get back the actual related object, so it could be an instance of Person or Company.

Implementation using One-to-Many

But what about when a Person or Company relates to many PhoneNumber objects?

class Person < ActiveRecord::Base
  has_many :phone_numbers, :as => :contact
end

Nothing tricky, just change has_one to has_many and pluralize the object name to :phone_numbers. Nothing else changes!

Usage

Usage stays just about the same:

@company.phone_numbers
@person.phone_numbers
@phone_number.contact

Now the .phone_numbers method returns an array. You can just think of it as a normal one-to-many!

Exercises

[TODO: Add Exercises]

References

@jcasimir

Copy link
Copy Markdown
Author

Good catch, thanks. Fixed!

@gmassanek

Copy link
Copy Markdown

How would this work with accepts_nested_attributes_for? I'm trying to use this in my application but it doesn't seem to be pushing the parent object to the child when using accepts_nested_attributes_for.

@jcasimir

jcasimir commented Jul 22, 2011 via email

Copy link
Copy Markdown
Author

@gmassanek

Copy link
Copy Markdown

But how about if delegating didn't really fit? In the example above, if you used accepts_attributes_for, everything would work except that it wouldn't fill in phone number's contact since it would be trying to set person_id. Am I trying to fit this in somewhere that it just might not work?

@jcasimir

jcasimir commented Jul 22, 2011 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment