How to safely rename STI models in Rails
Tags: ruby, rails
Published: 2023-08-18 18:39:32

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](https://github.com/rubyatscale/packs-rails), which meant that STI models's `type` values also had to be updated.

Shopify Engineering posted last year [a blog post](https://shopify.engineering/changing-polymorphic-type-rails) 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!

1. 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`](https://api.rubyonrails.org/v7.0.1/classes/ActiveRecord/Inheritance/ClassMethods.html#method-i-sti_name)), both the new and old type name are now supported
2. Migrate the data in the `type` column of all database records to reflect the new model name
3. 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:
```rb
# 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:
```rb
# 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:
```rb
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:
```rb
# 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](https://github.com/ankane/strong_migrations#backfilling-data)).
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:
```rb
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`](https://github.com/rubyatscale) internally)!

_Interested in what we do at Potloc? Come join us! [We are hiring](https://jobs.lever.co/Potloc?team=Engineering) 🚀_