Rails polymorphic associations and migrations
Posted by mde [ Thu, 03 Jan 2008 05:06:00 GMT ]
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 has_many_polymorphs 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 has_many_polymorphs 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 has_many_polymorphs 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
class TasksPerson < ActiveRecord::Base
belongs_to :person
belongs_to :assignment,
:polymorphic => true
end
class Person < ActiveRecord::Base
has_many_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_table :tasks_people do |t|
t.column :person_id, :integer
t.column :assignment_id, :integer
t.column :assignment_type, :string
end
create_table :people do |t|
t.column :name, :string
end
create_table :courses do |t|
t.column :name, :string
end
create_table :exams do |t|
t.column :name, :string
end
create_table :seminars do |t|
t.column :name, :string
end
end
def self.down
drop_table :tasks_people
drop_table :people
drop_table :courses
drop_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")
>> 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
class AssignmentsAssignee < ActiveRecord::Base
belongs_to :assignment, :polymorphic => true
belongs_to :assignee, :polymorphic => true
acts_as_double_polymorphic_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_table :assignments_assignees do |t|
t.column :assignment_id, :integer
t.column :assignment_type, :string
t.column :assignee_id, :integer
t.column :assignee_type, :string
end
create_table :people do |t|
t.column :name, :string
end
create_table :jobs do |t|
t.column :name, :string
end
create_table :organizations do |t|
t.column :name, :string
end
create_table :courses do |t|
t.column :name, :string
end
create_table :exams do |t|
t.column :name, :string
end
create_table :seminars do |t|
t.column :name, :string
end
end
def self.down
drop_table :assignments_assignees
drop_table :people
drop_table :jobs
drop_table :organizations
drop_table :courses
drop_table :exams
drop_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")
>> 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")
>> e.assignees.push(p)
>> e.assignees.push(o)
>> e.assignees.length => 2
Gotchas
One gotcha I noticed was that migrations wouldn't run to set up the database properly with the has_many_polymorphs 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.