Polymorphic might sound too advanced and you might think it will be a complicated one but, it isn’t, it's just a little twisted. Fear not, I will try my best to help you understand it as simply as possible. So without further ado, let's begin.

Topics to be covered in this article

The Foundation

All the 6 basic associations that I covered in Part 1 & Part 2 had one thing in common, all the tables we created were in some way connected either directly or indirectly through the associations and a foreign key.

However, in polymorphic, all tables are not connected but all tables are connected to a single table and that single table can belong to any table for each of its instances.

Does this mean that a single table would have multiple foreign key attributes in it to connect to other tables? And what if there were 100 tables connected to this single table, do we need to hard-code every 100 foreign keys to the database? And not to mention how terrible it would be to explicitly write 100x belongs_to association! Well, this is where Active Records’ Polymorphic comes to the rescue!

So its definition…

Polymorphic associations allows a model to “belong to” or link to multiple models through a single belongs_to association and a foreign key & type.

Yeah! you read that right! Doesn’t matter how many models/tables it's connected to, all we need is a single association, a foreign key, and its type.

Building tables

Let’s elaborate on the above definition with an example. Let's say: “A user uses either (belongs to) macOS, Windows, or Linux Operating System (OS), or in other words, each OS has multiple users.”

Clearly above example is a has_many-belongs_to relationship. We will create different tables for each OS so we will have 4 tables in total, 3 of OS (macOS, Windows, Linux), and a users table.

Creating OS tables in the database via migration:

rails g model MacOS name:string ver:string release_date:date
#####
rails g model WindowsOS name:string edition:string release_date:date
#####
rails g model LinuxOS distro:string ver:string release_date:date
#####

Foreign key and association name:

Before creating a user table, a question to ask here is how could a single foreign key (as per definition) link to multiple OS tables. The answer is that we don’t give any particular OS table name as an association name or foreign key, instead, we give an abstract name, a name that would represent all 3 OS tables we want to link to the user table.

os (operating system) would be a suitable foreign-key name and association name for our example, hence, os_id will be our foreign key attribute in the user table.

To get deeper understanding on naming convention for polymorphic association, I recommend this article.

Polymorphic attributes

Setting the name of a foreign key is sorted out but how would Rails know which particular OS an instance of a user belongs to with just an os_id attribute since it can link to any model?

This is sorted out by creating a polymorphic type attribute in the user table which will hold the value of a model class name it needs to link to. The name of this attribute will have a suffix of _type to whatever foreign key name we set, in our example, our foreign key name is os, so our polymorphic type attribute will be os_type.

With the help of these polymorphic attributes os_id and os_type, rails will be able to know which record I need to extract (through os_id) and from which table (through os_type).

In migration, creating both of these attributes is similar to how we would create a foreign key but with an addition of the polymorphic option to references to let Rails know we are declaring a polymorphic association:

rails g model User name:string os:references{polymorphic}
Rails automatically creates polymorphic attributes for us

After running migrations, our schema file will be updated accordingly.

A look at the tables and the attributes we created:

Setting up Association

For our example, the association we would use is has_many-belongs_to but a slight modification since we have an abstract foreign key name in the User table:

# app/model/mac_os.rb
class MacOs < ApplicationRecord
has_many :users, as: :os
end
# app/model/windows_os.rb
class WindowsOs < ApplicationRecord
has_many :users, as: :os
end
# app/model/linux_os.rb
class LinuxOs < ApplicationRecord
has_many :users, as: :os
end
# app/model/user.rb
class User < ApplicationRecord
belongs_to :os, polymorphic: true
end

Filling the data

Let’s assert some records in our tables through the rails console.

MacOs table:

# mac_1 = MacOs.create(name: 'Mavericks', ver: '10.9', release_date:"22/10/2013")
#####
# mac_2 = MacOs.create(name: 'Big Sur', ver: '11', release_date:"12/11/2020")

WindowsOs table:

# win_1 = WindowsOs.create(name: "Windows 7", edition:"Home", release_date:"1/10/2009")
#####
# win_2 = WindowsOs.create(name: "Windows 10", edition:"Education", release_date:"29/07/2015")

LinuxOs table:

# linux_1 = LinuxOs.create(distro: "Ubuntu", ver:"16.04.2 LTS", release_date:"16/02/2017")
#####
# linux_2 = LinuxOs.create(distro: "Debian", ver:"11", release_date:"14/08/2021")

User table:

Now let's create data of users and link the Operating Systems they use:

# user_1 = User.create(name: "Hatim", os: mac_1)
=> <User id: 1, name: "Hatim", os_type: "MacOs", os_id: 1, created_at: "##", updated_at: "##">
#####
# user_2 = User.create(name: "Khadija", os: win_2)
=> <User id: 2, name: "Khadija", os_type: "WindowsOs", os_id: 2, created_at: "##", updated_at: "##">
#####
# user_3 = User.create(name: "Ismail", os: linux_2)
=> #<User id: 3, name: "Ismail", os_type: "LinuxOs", os_id: 2, created_at: "##", updated_at: "##">

So we explicitly let Rails know which OS to link to a user through :os attribute and the value of it will be a class instance that holds the value of an instance of an OS. Rails will then automatically set os_id and os_type accordingly for that instance of a user.

A look at the data we created and how it links:

Extracting the data

Extracting Operating System data from the user:

# user_1.os
=> <MacOs id: 1, name: "Mavericks", ver: "10.9", release_date: "2013-10-22", created_at: "#", updated_at: "#">

Extracting all users of a particular OS:

#  win_2.users
=> [#<User id: 2, name: “Khadija”, os_type: “WindowsOs”, os_id: 2, created_at: “##”, updated_at: “##”>]>

Well, that’s all there is to know on how to set up a polymorphic association. I hope I have achieved to make you understand the topic thoroughly. If you have any questions or other comments, feel free to leave them below. Also, if you found this article useful, you can share it so others can find it as well.

Source Code

--

--