Polymorphic Associations with Ruby on Rails

Although not usually the first option for many, polymorphic associations are perfect for when you need to connect the same type of model to multiple other different models. In this article, we will create two different associations between three different models through polymorphism.

What is polymorphism

Polymorphism comes from Greek meaning “many forms”. Each programming paradigm has different ways of interpreting and implementing polymorphism. In object-oriented programming, it means that a piece of code can interact with multiple different types, be it through inheritance or through an interface.

On Ruby on Rails’ object-relational mapping (ORM) tool, ActiveRecord, polymorphism is a type of relation between models, where one model can belong to more than one other model, on a single association. In this context, the polymorphic class can adapt its form, through an interface, to suit all the classes that associate with it.

The Issue

Let’s think of two different objects that need to have their physical address stored. Like for instance, a marketplace that needs the shipping Address of a User and the physical Address of the Store to complete the shipment process.

One alternative for this would be to store it as a single json column called address in both models, but that can easily lead to unnecessary complexity and it’s really easy to have both fields end up looking different from each other over time. Also, every time we need to change something in the address, we would have to handle that in two different places, and if we ever create a new model that would also need to have an address it would be yet another column to deal with in synchronization with the other ones. And on top of that, json’s versatility can also be a problem, since it’s hard to create any form of validation for the field.

Adding separate foreign keys for User and Store might make sense at first, but if the project ever gets to a point where there will be more models containing an address, the address model would be hard to maintain, have poor readability and confusing business logic (having multiple foreign key columns but all of them being optional).

So in this scenario what we can do is create a single model, and any model that would need to have that information would inherit from it through an interface, and that’s where polymorphism comes in!

Through polymorphism, we can make a separate one-to-one relation between Address and User and another one-to-one relation with Store. In the next segment we will see how we can implement that using ruby on rails and Active Record.

The Implementation

In a migration of a polymorphic table, the only difference from your standard migration is the addition of the reference to its parent (a regular foreign_key column) and another column that will specify the type of polymorphic interface.

class CreateAddresses < ActiveRecord::Migration[7.0]
  def change
    create_table :addresses, id: :uuid do |t|
      t.string :line_one, null: false
      t.string :line_two
      t.string :city, null: false
      t.string :country, null: false
      t.string :state, null: false
      t.string :zip, null: false

      # Polymorphism related values
      t.bigint  :addressable_id
      t.string  :addressable_type

      t.datetime :discarded_at
      t.timestamps
    end
  end
end

This can be further simplified using the references key with the polymorphic flag from the migration form helper:

class CreateAddresses < ActiveRecord::Migration[7.0]
  def change
    create_table :addresses, id: :uuid do |t|
      t.string :line_one, null: false
      t.string :line_two
      t.string :city, null: false
      t.string :country, null: false
      t.string :state, null: false
      t.string :zip, null: false

      # Polymorphism related value
      t.references :addressable, polymorphic: true

      t.datetime :discarded_at
      t.timestamps
    end
  end
end

Both of these examples will generate the same table in the schema.

The Code

On the code side we’ll only need to specify the relation on the respective models like we would with any other association, but in this instance we specify the interface we declared on the migration- in this case we named it addressable:

class User < ApplicationRecord
  has_one :address, as: :addressable
  …
end
class Store < ApplicationRecord
  has_one :address, as: :addressable
  …
end

and on the Address model we need to declare a belong_to association with the interface that will connect with the other models.

class Address < ApplicationRecord
  belongs_to :addressable, polymorphic: true
end

And you’re all set! Now you will be able to create and reference the Address model for both User and Store. One important thing to note is that, when creating the Address for said models, you need to reference who it will connect to through the addressable key, i.e:

$ user = User.create!

$ Address.create(line_one: “K. West”, line_two: “5555”, city: “City”, state: “State”, country: “Country”, zip: “12345”, addressable: user)

It is also possible to create the address through its association:

$ User.create.build_address

=>
#<Address:0x00007fe66d4887c0
 id: nil,
 line_one: nil,
 line_two: nil,
 city: nil,
 country: nil,
 state: nil,
 zip: nil,
 addressable_type: "User",
 addressable_id: 1,
 created_at: nil,
 updated_at: nil>

And to call the address through the model you can call it like you would with any other relation, like so:

user = User.first
=>
user.address
=>
store = Store.first
=>
store.address
=>

You can also add the polymorphic association through forms using rails’ form helpers.

Since the user is new, he will not have an address, so we have to use the build method in order for the form display the fields

def new
  @user = User.new
  @user.address.build
end

and here’s how we implement the form using the fields_for tag:

  <%= form_with model: @user do |form| %>
  Address:
  <ul>
    <%= form.fields_for :address do |address| %>
      <li>
        <%= address_form.label :kind %>
        <%= address_form.text_field :kind %>

        <%= address_form.label :street %>
        <%= address_form.text_field :street %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

You’ll also need to allow the creation of the association through Useron its model class:

class User < ApplicationRecord
  has_one :address, as: :addressable
  accepts_nested_attributes_for :address
end

Testing it

To test polymorphism we can look for the same resources for when we test other common associations. Using rspec with the shoulda_matcher gem the expectations can look like this:

# models/address_spec.rb
describe “associations” do
    it { is_expected.to belong_to(:addressable) }
end

# models/user_spec.rb
describe “associations do
    it { should have_one(:address) }
end

# models/store_spec.rb
describe “associations do
    it { should have_one(:address) }
end

So there you have it, polymorphism is a very valuable tool for keeping your codebase DRY and consistent. Hopefully this post helped to clarify the idea behind the concept through one of its main uses.

We want to work with you. Check out our "What We Do" section!