Painless Polymorphic Associations

Evan Fujita
6 min readFeb 10, 2021

When working in Rails, polymorphic association offers a way to have a model belong to multiple models through an association — this is accomplished by allowing you to choose what model you select a foreign key from. If you’re wondering ‘how does that work?’ and ‘what would that look like?’, well, you came to the right place!

Preface

While there are more concise ways of demonstrating this example, I thought a step-by-step walkthrough would help give context to the association. So, to illustrate how this works, we’re going to start a new project: as our example, we’ll be using the presidential elections! But our parties here are unconventional: we have the Lebowski and Potter parties, and these will represent our two models. The third model will be the voter that associates itself with a single candidate from one or the other party.

Step One: Create the Project

rails new presidential_elections

Step Two: Establish MVC

rails g resource lebowski name quote
rails g resource potter name quote
rails g resource voter first_name last_name

If you’re unfamiliar with this procedure, then check this out! Basically, we are generating the skeleton for our MVC. The command ‘rails g resource…’ will create the model, the controller, a folder in app/views, all routes for the model, and a migration file with the column names that we entered (the column names will default to a string data type). As stated above, we have two main classes are the presidential parties — the Lebowski and Potter parties — and a third class: Voter.

An Aside!

So far we’ve built a basic three-model Rails project. Before we proceed, I want to mention that for the next steps we will see an unusual naming convention… We are not naming a variable, per se, or a model, but something of an indicator that communicates that we’re using a polymorphic association. The naming convention will take the polymorphic class name, in our case Voter, and append the suffix -able. In most cases we will have a word that naturally conforms to this that makes the name a little more fluid, which in our case would be ‘votable’ instead of ‘voterable’, so let’s use that. Okay, so we have this thing, ‘votable’— now what? We proceed!

Step Three: Establish Relationships in the Model

This is where our polymorphic associations start to take shape. So we have a model that can choose between two models… how will this relationship be structured? For us, the two parties Lebowski and Potter will both have many voters, with a caveat:

class Lebowski < ApplicationRecord
has_many :voters, as: :votable
end

And we will do the same for the Potter class:

class Potter < Application record
has_many :voters, as: :votable
end

This is similar to the has-many-through, except we are linking Lebowski and Potter classes to Voters not through a joiner table, but through the indicator :votable.

Conversely, we will declare the relationship that the Voter class has to the others:

class Voter
belongs_to :votable, polymorphic: true
end

And that’s all you need to do with the models! Not so bad, right?

Step Four: Alter Migration File and Migrate

We have the migration files that the generator created, and there’s just one thing we’ll need to change. For our Voter class, we have the following migration:

class CreateVoters < ActiveRecord::Migration[6.1]
def change
create_table :voters do |t|
t.string :first_name
t.string :last_name
t.timestamps
end
end
end

As with any association, we’re going to need to include some kind of foreign key id; however, since we are dealing with multiple models, we’re also going to need to include a foreign key type. The simplest way to do this would be to add a single line to our migration:

class CreateVoters < ActiveRecord::Migration[6.1]
def change
create_table :voters do |t|
t.string :first_name
t.string :last_name
t.references :votable, :polymorphic => true
t.timestamps
end
end
end

Now let’s migrate with ‘rails db:migrate’ and take a look at the schema:

create_table "voters", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "votable_type"
t.integer "votable_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["votable_type", "votable_id"], name: "index_voters_on_votable"
end

By adding a single line, ‘t.references :votable, :polymorphic => true’, to the migration, we produced three additional lines in our schema. We see our now-familiar term ‘votable’ again, accompanied by id and type.

That’s It! Sort of…

Congratulations — you’ve set up your first polymorphic association! You’ll notice that all of the work to create a polymorphic association resides in two steps: establishing the relationships in the model and altering the migration file. While this is sufficient for setting up the association, let’s look into how we will interact with these models.

Having migrated the voters table with the columns first_name, last_name, votable_id, and votable_type, we need all four of these to create a voter instance. We could randomly create a voter, but since I made a form I’ll include it here:

A sample ballot for candidates of the Lebowski and Potter parties

This is an example of how we can establish a relationship through a form. We have the voter, who in this case is the user, Harry Potter, and each instance of both the Lebowski and Potter classes are listed. When the form gets submitted here, there will be a new instance of voter with the name Harry Potter and it will be associated with the Arthur Weasley instance of the Potter class. How does this work?

  1. I opted for radio buttons as they force the user to make a single selection. And in selecting a candidate, we also select a votable_id;
  2. I delegated the form param specifically for the votable_type to the create method in the Voters controller, which looks like this:
def create
id = params[:voter][:votable_id]
candidate = Lebowski.find_by(id: id) || Potter.find_by(id: id)
votable_type = candidate.class.name
@voter = Voter.new(voter_params)
@voter.votable_type = votable_type
@voter.save
end

So as the voter submits the form, we’re getting all the required information. Now we can finally see what this will look like:

Voter.first=> #<Voter id: 1, first_name: "Harry", last_name: "Potter", votable_type: "Potter", votable_id: 3 ... >

(**For the sake of brevity I substituted the created_at and updated_at details with ellipses in this and future examples.) This looks exactly how we would expect it to… so how do we glean more information from this? We call on our old friend ‘votable’.

Voter.first.votable=> <Potter id: 3, name: "Arthur Weasley", quote: "Dark and difficult times lie ahead. Soon we must a...", ... >

By appending ‘votable’ to our voter instance, we have access to the associated Potter instance! (It’s worth mentioning here that the votable id is directly linked to the instance id with respect to its class. Through votable_type and votable_id, we see that Arthur Weasley is of Potter class and has an id of 3.) And from there we can further add to the chain to retrieve the instance id, name, or quote. Alternatively, if we want to see who voted for Arthur Weasley, we can do the following:

Potter.find_by(name: "Arthur Weasley").voters=> <Voter id: 1, first_name: "Harry", last_name: "Potter", votable_type: "Potter", votable_id: 3 ... >

Needless to say, had Arthur Weasley had more voters, we would see much more information… nevertheless, just as in the last example, we have access to all of Arthur Weasley’s voters’ data.

A Final Note

One glaring question is this: what if we conflated the party classes? After all, is it even necessary to have two separate party classes? In learning polymorphic associations I also learned that there are solutions, and there are appropriate solutions. To answer our question, of course we could restructure this project model to have a Candidates class, and perhaps assign a party to each candidate, effectively eliminating the need for a polymorphic model in the first place, but that’s not to say that it’s not a viable option all the same and that, depending on the larger context of the project, polymorphic associations might indeed by the appropriate solution.

Thank you for reading!

--

--