Aug 30, 2007Sometimes, 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 AssociationsIt 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:
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?
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
Promiscuous ModelsEssentially, 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:
You'll need a model, too:
create_table :name_aliases do |t| t.column :aliasable_type, :string t.column :aliasable_id, :integer t.column :name, :string 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.
class NameAlias < ActiveRecord::Base belongs_to :aliasable, :polymorphic => true end
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.
...will work just as it always has.
City.find_by_name("montreal").name_aliases.create :name => "mtl"
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 RockBecause 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?