Promiscuity for Fun and Profit


Aug 30, 2007
Sometimes, we advanced rails users take our knowledge of the framework for granted, have unfair expectations for less advanced rails coders. The worst examples of such unreasonable demands can be found on a daily basis in the IRC channel (#rubyonrails on Freenode, for the uninitiated). And, while the IRC channel can certainly be harsh, even those of us who don't throw around the term "noob" like it's going out of style sometimes (often) complain more than we help. As one of my peers likes to remind me, I'm present in that category more often than I'd like to admit (if I didn't have to). As an aside: While Gary is often the one I'm ranting to, his code is never the subject of the rant. I'd really like to turn my negative energy in to something positive. This will be the first of many articles that seek to add to the pool of resources on rails best practices, shortcuts, tips, and tricks. I hope you like it.

Today's Topic: Polymorphic Associations

It was a check-in today that sparked the round of complaining that sparked the round of Gary telling me to shut up that sparked the desire to write this article. The check-in added a bunch of tables to a project, with a schema that looked (something) like this:
create_table :cities do |t|
  t.column :name, :string
  ...
end

create_table :city_aliases do |t|
  t.column :city_id, :integer
  t.column :alias, :string
end

create_table :provinces do |t|
  t.column :name, :string
  ...
end

create_table :province_aliases do |t|
  t.column :province_id, :integer
  t.column :alias, :string
end
Well, it's definitely not the worst schema ever, but it can be improved drastically with a polymorphic association. What the heck is that, you ask?

Promiscuous Models

Essentially, a polymorphic association is a model that can belong_to any other table. Polymorphic associations are the way that a lot of popular plugins work. Ever wonder how acts_as_taggable just magically adds tagging to any model, without any migration? Well, it's super easy. Just replace your duplicate tables with one of these babies:
create_table :name_aliases do |t|
  t.column :aliasable_type, :string
  t.column :aliasable_id, :integer
  t.column :name, :string
end
You'll need a model, too:
class NameAlias < ActiveRecord::Base
  belongs_to :aliasable, :polymorphic => true
end
Note: I chose the name NameAlias because alias is a reserved word in ruby, and it may cause you some problems if you try to name a model that.

Going back to our example, we can redefine our models as follows:
class City < ActiveRecord::Base
  has_many :name_aliases, :as => :aliasable
end

class Province < ActiveRecord::Base
  has_many :name_aliases, :as => :aliasable
end

Now What?

A polymorphic association works just like any normal association. That means you can use all of your favorite AssociationProxy methods, and everything.
City.find_by_name("montreal").name_aliases.create :name => "mtl"
...will work just as it always has.

How'd you do that?

When you tell rails that :polymorphic => true, in your belongs_to options, it knows that it needs to store the id and the type of the associated class. You give the association a generic name like aliasable or taggable, to let rails know what the fields are named in the database. For example, if you call your association aliasable, you need to have the fields aliasable_type and aliasable_id. Make sure aliasable_type is a long enough string column to store the name of your longest model in it. In production, both of these fields should probably be indexed. When you associate a class with your polymorphic model, you have to tell it to associate as your generic association name, instead of its own, to let rails know how to join the tables up properly: :as => :aliasable.

Name Aliases that Rock

Because this example is a particularly useful one, it's worth showing you a method that I write often. It will work with any association, but I often use it with aliases. It adds an extra finder, which allows you to find_by_name_alias.
class Person
  has_many :name_aliases, :as => :aliasable
    
  def self.find_by_name_alias(name)
      Person.find(:all, :include => :name_aliases, :conditions => ["name_aliases.name = ?", name])
  end
end

Feedback, please!

So, there you have it: polymorphic associations. This is where you come in. I have some questions:
  • How'd you like it?
  • What could be improved?
  • Should I use a different format (like a screencast)?
  • Are there any topics you'd like to see?
Thanks for reading.