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 CreateVotes
migration:
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 @actor.votes
, @movie.votes
and @director.votes
, as expected. To retrieve a Vote object’s parent, we can call @vote.voteable
, which returns its corresponding Actor
, Movie
, or 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.
Summary (tldr)
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 /
belongs_to
declarations
2. Ask yourself what these models have in common
- Describe that commonality in a single trait, like
Commentable
orLikeable
- Use this shared trait to name the polymorphic association
3. Build model migrations and associations
- Model migrations: use
awardable_id
andawardable_type
columns - Model associations: use
belongs_to :awardable, polymorphic: true
andhas_many :awards, as: :awardable