DEV Community

Jérôme Parent-Lévesque for Potloc

Posted on

How to safely rename STI models in Rails

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!

  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), 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:

# models/aerospace/rocket.rb
module Aerospace
  class Rocket < Vehicle
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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_names, 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)