In Rails, Single Table Inheritance (STI) models store their full model name (including any module namespaces) in a type
column. This column is used by ActiveRecord to determine which model to instantiate when loading a record from the database. This means that renaming such models isn't as easy as just changing the class name; it must also involve a data migration to update the values stored as type
. However, how can we safely perform this in a live, production environment?
This is a challenge that we recently ran into at Potloc while working on modularization of our codebase. This involved namespacing all of our models under packs, which meant that STI models's type
values also had to be updated.
Shopify Engineering posted last year a blog post about this same issue (albeit for Polymorphic models) in which they suggest to change entirely the nature of what is stored as type
in the database. However, they mention that:
Our solution adds complexity. It’s probably not worth it for most use cases
And this was indeed how we felt for our use case. We wanted to perform this in a way that would have no impact on the way Rails works, and all while having zero downtime.
The Solution
Let's jump right in to the final solution for those who don't need all the details and just want a quick step-by-step guide!
- In a first deployment;
- Rename the model to whatever you need
- Create, using the old model name, a new model that inherits from the renamed model but that is otherwise empty
- Remove all uses of the old model in the codebase
- Make sure that everywhere the
type
name was being used (whether as a raw string or through#sti_name
), both the new and old type name are now supported
- Migrate the data in the
type
column of all database records to reflect the new model name - In a final deployment, remove the deprecated classes and old
type
names used in the codebase
Step 1: Renaming the model
To help navigating through these steps, let's use a simple example:
Your team is currently modularizing the codebase and wants to create a new pack for their aerospace 🚀 division. You are therefore tasked to move an STI model named Rocket
(say this model is under a base Vehicle
model and vehicles
database table) into a new namespace: Aerospace::Rocket
.
You can start by renaming the model directly:
# models/aerospace/rocket.rb
module Aerospace
class Rocket < Vehicle
# ...
end
end
Then, here comes the neat trick: We will create a sub-type of Aerospace::Rocket
using the old model name:
# models/rocket.rb
class Rocket < Aerospace::Rocket; end
Notice that this model is completely empty. In fact, we shouldn't use it anywhere in the codebase (except for its #sti_name
, we'll come back to that later).
This is not by accident. It turns out that ActiveRecord, under the hood, will use the sti_name
of the current model, as well as the sti_name
of any child models when querying records!
This means that by making the old model name inherit from the new one, we get for free the following behaviour:
Aerospace::Rocket.all.to_sql
# => SELECT * FROM vehicles WHERE type IN ('Aerospace::Rocket', 'Rocket');
This will therefore pave the way for us to then run a data migration that changes all Rocket
types stored in the database to Aerospace::Rocket
without breaking anything! 🎉
But before we do that, we have to take care of a couple more cases.
First, we want all new records created to use the new type name. This simply means replacing all uses of Rocket
by Aerospace::Rocket
in the codebase.
Second, if this model's #sti_name
or its raw string ("Rocket") were used anywhere (for example in active record queries) we now have to make sure to support both the new and the old names.
In a typical ActiveRecord query, this might look something like this:
# From:
fleet.vehicles.where(type: Rocket.sti_name)
# To:
fleet.vehicles.where(type: [Aerospace::Rocket.sti_name, Rocket.sti_name])
# Or, better yet:
Aerospace::Rocket.where(fleets: fleet)
However, there might be other instances in your code where you might be using the #sti_name
in a different way. You'll need to individually take a look at each of these. For example, since at Potloc we are using GraphQL and have some Enum
types defined for STI models, we had to make sure that both possible type
values would coerce to the same enum value that is sent back from the API.
Step 2: The data migration
That was the hard part! After step 1 is deployed, the rest is pretty much just business-as-usual when working in a continuous deployment environment.
In this step, we need to rename all old type names stored in the database to the new one. We can achieve this with a data migration (a good guide for this is the strong-migrations gem readme).
Note that this step may vary depending on your team's choice of how to run data migrations, but no matter the approach the following command (or equivalent) needs to be run in the production environment:
Vehicle.where(type: Rocket.sti_name).update_all(type: Aerospace::Rocket.sti_name)
Step 3: Cleanup
We should now be at a point where no records in the database are using the old sti_name
anymore and any newly created records are all stored using the new name as type
.
We can therefore cleanup everything!
First, we can remove the old Rocket
model (the one that was empty and inherited from Aerospace::Rocket
).
And finally, we can remove any special logic we added in Step 1 to support both Rocket.sti_name
and Aerospace::Rocket.sti_name
to now only support the latter.
And that's it! Migration complete! 🔥
Conclusion
It took a few steps, but by leveraging Rails' mechanism that fetches database records matching any of a model's children #sti_name
s, we were able to rename our Rocket
model:
- without any downtime, and;
- without any changes to Rails' handling of STI models
Additionally, although this blog post didn't cover it, a similar process can also be used for renaming models used in Polymorphic associations. This might be the subject of a future article.
Hopefully this guide can help you to easily rename STI models, especially when it comes to modularization of your large Rails monoliths (something we can strongly recommend after a few months of trying packs-rails
internally)!
Interested in what we do at Potloc? Come join us! We are hiring 🚀
Top comments (0)