In this post I'm going to share how my teammates and I redefined the way we store one of the polymorphic associations in the Shopify codebase. I am part of the newly formed Payment Flexibility team. We work on features that empower merchants to better manage their payments and receivables on Shopify.
Code at Shopify is organized in components. As a new team, we decided to take ownership over some existing code and to move it under the component we’re responsible for (payment flexibility). This resulted in moving classes (including models) from one module to another, meaning their namespace had to change. While thinking about how we were going to move certain classes under different modules, we realized we may benefit from changing the way Rails persists a polymorphic association to a database. Our team had not yet entirely agreed on the naming of the modules and classes. We wanted to facilitate name changes during the future build phase of the project.
We decided to stop storing class names as a polymorphic type for certain records. By default, Rails stores class names as polymorphic types. We decided to instead use an arbitrary string. This article is a step by step representation of how we solved this challenge. I say representation because the classes and data used for this article are not taken from the Shopify codebase. They’re a practical example of the initial situation and the solution we applied.
I’m going to start with a short and simple reminder of what polymorphism is, then move on to a description of the problem, and finish with a detailed explanation of the solution we chose.
What is Polymorphism?
Polymorphism means that something has many forms (from the Greek “polys” for many and “morphē” for form).
Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.
For the purpose of this post, I’ll take the example of a Vehicle
that has_one :key and
the Key belongs_to :vehicle
.
A Vehicle
can be a Car
or a Boat
.
You can see here that Vehicle
has many forms. The relationship between Key
and Vehicle
is polymorphic.
The foreign key stored on the child object (the Key
record in our example) points to a single object (Vehicle
) that can have different forms (Car
or Boat
). The form of the parent object is stored on the child object under the polymorphic_type
column. The value of the polymorphic_type
is equal to the class name of the parent object, "Car"
or "Boat"
in our example.
The code block below shows how a polymorphic association is stored in Rails.
The Issue
As I said initially, our vehicle
classes had to move under another module, a change in module results in a different namespace. For this example I’ll pretend I want to change how our code is organized and put Car
under the Garage
module.
I go ahead and move the Car
and Boat
models under the new module Garage
:
I’m now running into the following:
The vehicle_type
column now contains "Garage::Car"
, which means we’ll have vehicle_type: "Car"
and vehicle_type: "Garage::Car"
both stored in our database.
Having these two different vehicle_type
values means the Key
records with vehicle_type: "Car"
won’t be returned when calling a_vehicle.key
. The Active Record association has to be aware of all the possible values for vehicle_type
in order to find the associated record:
Both these vehicle_type
values should point towards the updated model Garage::Car
for our polymorphic ActiveRecord association to continue to work. The association is broken in both directions. Calling #vehicle
on a Key
record that has vehicle_type: "Car"
won’t return the associated record:
The Idea
Once we realized changing a namespace was going to introduce complexity and a set of tasks (see next paragraph), one of my teammates said to me, “Let's stop storing class names in the database altogether. By going from a class name to an arbitrary string we could decrease the coupling between our codebase and our database. This means we could more easily change class names and namespaces if we need to in the future.” For our example, instead of storing "Garage::Car"
or "Garage::Boat"
why don't we just store "car"
or "boat"
?
To go forward with a module and classes name change, without modifying the way Active Record stores a polymorphic association, we would have to add the ability to read from several polymorphic types when setting the ActiveRecord association. We also would have had to update existing records for them to point to the new namespace. If we go back to our example, records with vehicle_type: "Garage::Car"
should point towards the new Garage::Car
model until we could perform a backfill of the column with the updated model class name.
In Practice: Going From Storing a Class Name to an Arbitrary String
Rails has a way to override the writing of a polymorphic_type
value. It’s done by redefining the polymorhic_name
method. The code below is taken from the Rails gem source code:
Let's redefine the source code above for our Garage::Car
example:
When creating a Key
record we now have the following:
Now we have both "Car"
the class name and "car"
the arbitrary string stored as vehicle_type
. Having two possible values for vehicle_type
brings another problem. In a polymorphic association, the target (associated record) is looked up using the single value returned in .polymorphic_name
, and this is where the limitation lies. The association is only able to look for one vehicle_type
value. vehicle_type
is stored as the value returned by polymorphic_name
when the record was created.
An example of this limitation:
Look closely at the SQL expression, and you’ll see that we’re only looking for keys with a vehicle_type = "car"
(the arbitrary string). The association won’t find the Key
for vehicles created before we started our code change (keys where vehicle_type = "Car"
). We have to redefine our association scope so it can look for keys with vehicle_type
of "Car"
or "car"
:
Our association now becomes the following SQL expression:
The association is now looking up keys with either "car"
or "Car"
as vehicle_type
.
Now that we can read from both the class name and new arbitrary string as a vehicle_type
for our association, we can go ahead and clean up our database to only have arbitrary strings stored as vehicle_type
. At Shopify, we use MaintenanceTasks. You could run a migration or a script as the one below to update your records.
Once the clean up is complete, we only have arbitrary strings stored as vehicle_type
. We can go ahead and remove the .unscope
on the Garage::Car
and Garage::Boat
association.
But Wait, All This for What?
The main benefit from this patch is that we reduced the coupling between our codebase and our database.
Not storing class names as polymorphic types means you can move your classes, rename your modules and classes, without having to touch your existing database records. All you have to do is update the class names used as keys and values in the three CLASS_MAPPING
hashes. The value stored in the database will remain the same unless you change the arbitrary strings these classes and class names resolve to.
Our solution adds complexity. It’s probably not worth it for most use cases. For us it was a good trade off since we knew the naming of our modules and classes could change in the near future.
The solution I explained isn’t the one we initially adopted. We initially went an even more complex route. This post is the solution we wish we had found when we started looking into the idea of changing how a polymorphic association is stored. After a bit of research and experimentation, I came to this simplified version and thought it was worth sharing.
Diego is a software engineer on the Payment Flexibility Team. Living in the Canadian Rockies.
We want your feedback! Take our reader survey and tell us what you're interested in reading about this year.
If building systems from the ground up to solve real-world problems interests you, our Engineering blog has stories about other challenges we have encountered. Visit our Engineering career page to find out about our open positions. Join our remote team and work (almost) anywhere. Learn about how we’re hiring to design the future together—a future that is digital by default.