Rails polymorphic associations and migrations

2008-01-03 07:06:00

In a Rails app I was recently working on, the data model called for associating sets of related business objects. Initially it looked like I was going to have to handle it with sub-classing.

Sub-classing and prog-rock keyboardists

I was kind of reluctant to do this in a Rails app, because the traditional Rails approach to representing an object hierarchy uses single-table inheritance -- dumping a column for every possible attribute for every possible sub-class into one table.

This kind of ugliness is an example of the Rick Wakeman Brute Force Approach: no worries about those primitive synths that can only provide one patch at a time, just drag an ass-load of them with you whenever you go on the road. It gets the job done, but it's not particularly elegant.

In my particular case, sub-classing might have actually worked, if I could have endured the single-table-inheritance stink. But what about cases you may want to group types of things in your app that have a less obvious relationship than can be represented with a hierarchy chain?

Promiscuity is so much nicer

Thankfully, a very nice way to do this sort of thing exists in the form of polymorphic associations (also called "promiscuous associations," yay!), which allow you a lot of the same capabilities as an inheritance hierarchy, but gives you more flexibility in creating relationships between objects.

With polymorphic associations, you can literally connect any type of thing in your data model to any other type. A common example used to describe this is tagging, where a given tag could potentially be associated with any type of thing in your data model.

It almost makes you a little drunk with power, this newfound ability to link this to that and that to the other thing over there. You can almost imagine, with all due apologies to the Pythons, a Royal Society for Associating Things With Other Things.

The hasmanypolymorphs plugin

Using polymorphic associations without any special help is possible in Rails, but it is somewhat limited. I found that what I needed to do required the use of the excellent hasmanypolymorphs plugin developed by Evan Weaver.

If you find yourself reading the docs and getting utterly confused the first time through, as I did, you can check out Pratik Naik's helpful tutorial that does a really nice job of laying it all out.

Dude, where's my DB?

The one piece I found missing in all the docs and tutorials for the hasmanypolymorphs plugin was anything that showed what the corresponding database tables should look like, or how to use Rails migrations to set them up.

(There is a helpful, albeit minimal, example in the plugin source code examples/ directory for a simple single-sided association that pointed me in the right direction.)

I figured it might be helpful for other people using the plugin to see some migration examples. It might save them some of the time I spent futzing with things to get it all working.

Single-sided associations

Single-sided polymorphic associations are the simpler of the two kinds, where many types of things are associated with a single other type.

Say I'm writing an e-learning app, and I have courses, exams, and seminars, all of which might be assigned to a person to take. My model would look like this:

class Course < ActiveRecord::Base end class Exam < ActiveRecord::Base end class Seminar < ActiveRecord::Base end</p> <p>class TasksPerson < ActiveRecord::Base belongs<em>to :person belongs</em>to :assignment, :polymorphic => true end</p> <p>class Person < ActiveRecord::Base has<em>many</em>polymorphs :assignments, :from => [:courses, :exams, :seminars], :through => :tasks_people end

And the migration would look like this:

class TasksPeople < ActiveRecord::Migration def self.up create<em>table :tasks</em>people do |t| t.column :person<em>id, :integer t.column :assignment</em>id, :integer t.column :assignment<em>type, :string end create</em>table :people do |t| t.column :name, :string end create<em>table :courses do |t| t.column :name, :string end create</em>table :exams do |t| t.column :name, :string end create<em>table :seminars do |t| t.column :name, :string end end def self.down drop</em>table :tasks<em>people drop</em>table :people drop<em>table :courses drop</em>table :exams drop_table :seminars end end

Once you've got that set up, you can try it out in the console to see if stuff is working:

>> c = Course.create(:name => "How Not To Get Eaten By An Alligator")</p> <blockquote> <blockquote> <p>e = Exam.create(:name => "Zombie Army Attack Preparedness") p = Person.create(:name => "Ravi Shankar") p.assignments.push(c) p.assignments.push(e) p.assignments.length => 2

Woohoo, Ravi has two assignments, just like you'd expect.

The beauty of this is that now I can assign courses, exams, seminars -- or anything else I deem "tasky" -- to people in my app, without having to wrangle the unwieldiness of shoving all that taskiness into a single table.

Double-sided associations

This is where it gets all wacky -- where multiple different types of things can be associated with other, multiple different types of things. Mass chaos ensues -- but in a good way.

Imagine now that e-learning app has grown and I need to make more complicated kinds of assignments. I need to be able to assign those different types of tasks (courses, exams, seminars) to individuals, or to groups -- say, for example, to people, jobs, and organizations.

(For anyone who worked with me back in the KnowledgeWire days, this is a nice trip down memory lane.)

This is a good example of a case where simple sub-classing would be conceptually awkward and weird -- people, jobs, and entire organizations aren't particularly similar things. The only thing they really have in common in this data model is their ability to have things assigned to them.

The model code would look like this:

class Course < ActiveRecord::Base end class Exam < ActiveRecord::Base end class Seminar < ActiveRecord::Base end class Person < ActiveRecord::Base end class Job < ActiveRecord::Base end class Organization < ActiveRecord::Base end</p> <p>class AssignmentsAssignee < ActiveRecord::Base belongs<em>to :assignment, :polymorphic => true belongs</em>to :assignee, :polymorphic => true</p> <p>acts<em>as</em>double<em>polymorphic</em>join( :assignments =>[:courses, :exams, :seminars], :assignees =>[:people, :jobs, :organizations] ) end

And the migration would be like this:

class AssignmentsAssignees < ActiveRecord::Migration def self.up create<em>table :assignments</em>assignees do |t| t.column :assignment<em>id, :integer t.column :assignment</em>type, :string t.column :assignee<em>id, :integer t.column :assignee</em>type, :string end create<em>table :people do |t| t.column :name, :string end create</em>table :jobs do |t| t.column :name, :string end create<em>table :organizations do |t| t.column :name, :string end create</em>table :courses do |t| t.column :name, :string end create<em>table :exams do |t| t.column :name, :string end create</em>table :seminars do |t| t.column :name, :string end</p> <p>end def self.down drop<em>table :assignments</em>assignees drop<em>table :people drop</em>table :jobs drop<em>table :organizations drop</em>table :courses drop<em>table :exams drop</em>table :seminars end end

Now let's hop back into the console and see if this stuff all works:

>> c = Course.create(:name => "Life and Times of the Three Stooges")</p> <blockquote> <blockquote> <p>s = Seminar.create(:name => "LARPing for Fun and Profit") p = Person.create(:name => "Henry Kissinger") o = Organization.create(:name => "Australopithecus Unlimited") p.assignments.push(c) p.assignments.push(s) p.assignments.length => 2 o.assignments.push(c) o.assignments.push(s) o.assignments.length => 2

Very nice. I can give the same assignments to both Henry Kissinger and to the entire Australopithecus Unlimited organization.

I can assign any of these tasky things to a person, job, or organization in the app. And also interestingly, I can do the reverse:

>> e = Exam.create(:name => "Apple Fanboy Test")</p> <blockquote> <blockquote> <p>e.assignees.push(p) e.assignees.push(o) e.assignees.length => 2


One gotcha I noticed was that migrations wouldn't run to set up the database properly with the hasmanypolymorphs plugin loaded. Looks like it's some kind of circular dependency issue. In any case, leaving the plugin require statement out of environment.rb until after setting up the DB is a workaround for this problem.

Wrapping up

There you have it. A nice, flexible way to create associations between loosely related types in your model. No ugly sub-classing stuff by trying to shoehorn every related type into one table.

There are some other nice tutorials and docs online to help you, but I couldn't find any that include the migration scripts above. I hope this helps save somebody some time.


This is the blog for Matthew Eernisse. I currently work at Yammer as a developer, working mostly with JavaScript. All opinions expressed here are my own, not my employer's.


Previous posts

All previous posts ยป

This blog is a GeddyJS application.