Has_many_polymorphs is a rockin’ Rails plugin. But sometimes it’s like:

You hear about the plugin, and instead of “this is fuckin’ sweet!”, you might be like “pfff whatever”. But that kind of thinking is just abetting the enemy. Be prepared. The need will arise.
So… I’m going to prepare you to use “has_many_polymorphs”. I’ll take a top-down approach for this tutorial (my first tutorial…bitches!):
Use case
Consider the following example.
We have a Person. A Person can own several types of items: Dvds, Books, Cars, Ferraris in all colors. Ferraris are not Cars; they clearly deserve their own model.
Maybe:
1 2 3 4 5 6 7 |
class Person < ActiveRecord::Base has_many :items end class Book < ActiveRecord::Base # Dvd, Car, etc. belongs_to :person end |
Hey great! If only it would work. What class is :items supposed to use? No one knows.
The table jungle
Our next instinct might be to create a join model and use a has_many :through association.
Our models would look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Person < ActiveRecord::Base has_many :dvd_ownerships has_many :car_ownerships has_many :dvds, :through => :dvd_ownerships has_many :cars, :through => :car_ownerships end class DvdOwnership < ActiveRecord::Base belongs_to :person belongs_to :dvd end class CarOwnership < ActiveRecord::Base belongs_to :person belongs_to :car end class Dvd < ActiveRecord::Base has_many :dvd_ownerships has_many :people, :through => :dvd_ownerships end class Car < ActiveRecord::Base has_many :car_ownerships has_many :people, :through => :car_ownerships end |
Well, this is weak. We need a separate, yet identical join table for every item type. This would make our database a table jungle. Let’s be a bit smarter and use just one join table.
Rails way; broken way
Rails has a sneaky feature called Polymorphic Associations which could be very useful in situations like this. In a few words, polymorphic associations are unclassed and can be connected to any model.
In order to use polymorphic associations, our models should apparently look like below. Please note this code will not work. Dammit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Person < ActiveRecord::Base has_many :ownerships, :as => :ownable has_many :dvds, :through => :ownerships has_many :cars, :through => :ownerships end class Ownership < ActiveRecord::Base belongs_to :person belongs_to :ownable, :polymorphic => true end class Dvd < ActiveRecord::Base has_many :ownerships, :as => :ownable has_many :people, :through => :ownerships end class Car < ActiveRecord::Base has_many :ownerships, :as => :ownable has_many :people, :through => :ownerships end |
What’s wrong? In the Person model, we have has_many :dvds, :through => :ownerships association defined. ActiveRecord will then try to find the :dvds association in the :source model (Ownership). But ActiveRecord provides no way to specify that an association has a several different sources when viewed through a has_many :through.
Well maybe you could do some ActiveRecord internals hacking, or use a bunch of SQL conditions, and somehow make it work. Maybe. Definitely no fun either way.
has_many_polymorphs to the rescue
So let’s call has_many_polymorphs ! It’s an emergency!
script/plugin install svn://rubyforge.org/var/svn/fauna/has_many_polymorphs/trunk |
It’s arrived… but can it solve our problem?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Person < ActiveRecord::Base has_many_polymorphs :ownables, :from => [:dvds, :cars, :books], :through => :ownerships end class Ownership < ActiveRecord::Base belongs_to :person belongs_to :ownable, :polymorphic => true end class Dvd < ActiveRecord::Base end class Car < ActiveRecord::Base end class Book < ActiveRecord::Base end |
“Excuse me! WTF just happened?“
3 lines of model code, instead of a shrubbery of SQL. Sweet.
what just happened
has_many_polymorphs is has_many :through for polymorphic associations.
There’s a lot of magic here. For explanation, we’ll use following terminology mapping :
- Parent model -> Person
- Join model -> Ownership
- Child models -> Dvd, Car (These are the models you specify in the :from key of has_many_polymorphs )
has_many_polymorphs sets up a shitload of associations for you just from that one method call:
- a magical polymorphic has_many :through association in the parent model that includes all the children. E.g. Person#ownables. (This is actually its own association type, but it’s just like a has_many :through.)
- a has_many association for the join model in the parent model. E.g has_many :ownerships in the Person model. This is a normal has_many association using the parent_id as a foreign key in the join. (Remember how we said belongs_to :person in the Ownership model.)
- a polymorphic has_many association for the join model in all child models. E.g has_many :ownerships, :as => :ownable in Dvd, Car models.
- a bunch of has_many :through associations for all children supplied in :from in parent. E.g has_many :dvds and has_many :cars in Person model
- a bunch of has_many :through associations in all children supplied in :from for parent. E.g. has_many :people in Dvd and Car models.
The last bits are tricky. Even though you have defined a has_many_polymorphs associations in parent model ( Person ), it dynamically injects associations into the child models ( Dvd, Car ) as well.
If you turn on a has_many_polymorphs debugging option ( ENV[‘HMP_DEBUG’] to true), it’ll show you the generated associations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Person< ActiveRecord::Base has_many :ownerships, :dependent => :destroy, :foreign_key => "person_id", :class_name => "Ownership" has_many :dvds, :source => :ownable, :through => :ownerships, :source_type => "Dvd", :class_name => "Dvd" has_many :cars, :source => :ownable, :through => :ownerships, :source_type => "Car", :class_name => "Car" end class Dvd < ActiveRecord::Base has_many :ownerships, :dependent => :destroy, :as => :ownable has_many :people, :source => :person, :foreign_key => "person_id", :through => :ownerships, :class_name => "Person" end class Car < ActiveRecord::Base has_many :ownerships, :dependent => :destroy, :as => :ownable has_many :people, :source => :person, :foreign_key => "person_id", :through => :ownerships, :class_name => "Person" end |
However, this is just to give you a rough idea. has_many_polymorphs extends some of the associations to add more functionality and make them work even harder for you.
Hey let’s use it already
Now you can do things with the parent object like:
1 2 3 4 5 6 7 8 9 10 |
# Buy a new car! >> p = Person.find(:first) >> p.cars << Car.create(:name => 'Ferrari') >> p.cars.count => 1 >> p.dvds << Dvd.create(:name => "Hello world") >> p.dvds.count => 1 >> p.ownables.count => 2 |
And the same for the child object:
1 2 3 4 5 6 |
>> d = Dvd.find(:first) >> d.people.count => 1 >> d.people << Person.create(:name => "Neo") >> d.people.count => 2 |




