AcademyAwardable Polymorphic Associations
If you ever find yourself in a Rails-situation where you need one model to belong to multiple other models, consider using an ActiveRecord polymorphic association. Don’t let the multisyllabic name fool you; polymorphic associations aren’t as complex to build as they might seem. You can do it.
Let’s consider a completely relatable and engaging example: The Academy Awards. Maybe you’re building an Oscar ballot app for a family member who’s particularly obsessed with this time-honored, hallowed awards show. Your app at minimum would need to contain Actor, Movie, Director, and Vote models (plus all those technical award categories, but we’re going to ignore them for now - just like the Academy). We’ll need some savvy schema design to keep things simple and DRY.
Let’s start by trying to set up our schema the old-fashioned, non-polymorphic way first. Actor, Movie, and Director would all
have_many Votes, which means Vote would
belong_to Actor, Movie, and Director. Here’s how that would look in our
class CreateVotes < ActiveRecord::Migration def change create_table :votes do |t| t.integer :actor_id t.integer :movie_id t.integer :director_id t.timestamps null: false end add_index :votes, :actor_id add_index :votes, :movie_id add_index :votes, :director_id end end
And in our Vote model:
class Vote < ActiveRecord::Base belongs_to :actor belongs_to :movie belongs_to :director end
Man, I haven’t seen that much gratuitous repetition since Chris Nolan’s Memento. Let’s DRY this up a bit with a polymorphic association.
First step, we need to ask ourselves what our non-Vote models have in common. Or better, what common trait do they all share? In this case, they’re all able to be voted on - that is, they’re
Voteable. Our polymorphic association is born.
We can update our
CreateVotes migration as follows:
class CreateVotes < ActiveRecord::Migration def change create_table :votes do |t| t.integer :voteable_id t.string :voteable_type t.timestamps null: false end add_index :votes, :voteable_id end end
Note: You can simplify the migration even further by using
t.references; however, be careful here you’re not losing clarity along with a few lines of code.
class CreateVotes < ActiveRecord::Migration def change create_table :votes do |t| t.references :voteable, polymorphic: true, index: true t.timestamps null: false end end end
Notice that instead of adding explicit foreign keys for Actor, Movie, and Director, we’re instead adding a single
imageable_id foreign key, which Rails will automagically couple with the
imageable_type field to reference the correct corresponding model.
Speaking of models, ours look much better now with our new polymorphic-association-enabled, DRY-er declarations:
class Vote < ActiveRecord::Base belongs_to :voteable, polymorphic: true end class Actor < ActiveRecord::Base has_many :votes, as: :voteable # has_all_the :votes, as: :MerylStreep end class Movie < ActiveRecord::Base has_many :votes, as: :voteable end class Director < ActiveRecord::Base has_many :votes, as: :voteable end
Thanks to our polymorphic
belongs_to: :voteable declaration, we now have access to all kinds of fun methods on our models. We can call
@director.votes, as expected. To retrieve a Vote object’s parent, we can call
@vote.voteable, which returns its corresponding
Director. Throw a User class in there - plus some simple routes, controllers, and views - and we have the makings of a pretty functional Oscar ballot app - - - but that’s the topic for another (future) post. In the meantime, bravo. You’ve built an award-worthy polymorphic association.
1. Do you have one model that
belongs_to many other models?
- Does this one model only exist in relation to the other models?
Examples: Comments and Posts; Likes and Tweets
- Think about using polymorphic association instead of multiple foreign keys /
2. Ask yourself what these models have in common
- Describe that commonality in a single trait, like
- Use this shared trait to name the polymorphic association
3. Build model migrations and associations
- Model migrations: use
- Model associations: use
belongs_to :awardable, polymorphic: trueand
has_many :awards, as: :awardable