In Rails, Single Table Inheritance (STI) models store their full model name (including any module namespaces) in atype
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 astype
. 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 underpacks, which meant that STI models'stype
values also had to be updated.
Shopify Engineering posted last yeara blog post about this same issue (albeit for Polymorphic models) in which they suggest to change entirely the nature of what is stored astype
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 haveno 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 modelthat 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 namedRocket
(say this model is under a baseVehicle
model andvehicles
database table) into a new namespace:Aerospace::Rocket
.
You can start by renaming the model directly:
# models/aerospace/rocket.rbmoduleAerospaceclassRocket<Vehicle# ...endend
Then, here comes the neat trick: We will create a sub-type ofAerospace::Rocket
using the old model name:
# models/rocket.rbclassRocket<Aerospace::Rocket;end
Notice that this model is completely empty. In fact, we shouldn't use itanywhere 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 thesti_name
of the current model,as well as thesti_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 allRocket
types stored in the database toAerospace::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 ofRocket
byAerospace::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 someEnum
types defined for STI models, we had to make sure that both possibletype
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 thestrong-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 oldsti_name
anymore and any newly created records are all stored using the new name astype
.
We can therefore cleanup everything!
First, we can remove the oldRocket
model (the one that was empty and inherited fromAerospace::Rocket
).
And finally, we can remove any special logic we added in Step 1 to support bothRocket.sti_name
andAerospace::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 ourRocket
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 tryingpacks-rails
internally)!
Interested in what we do at Potloc? Come join us!We are hiring 🚀
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse