Salsify Engineering Blog

Active Record Migrations on Tables Leveraging STI

PSA: Active Record Migrations in Tables Leveraging Single Table Inheritance

I wanted to share a bit of information that we found interesting when writing active record migrations on tables leveraging STI (Single Table Inheritance). We came across a not so obvious gotcha with STI models re-defined in migration classes and wanted to explore our thought process while solving the subsequent problems.

We want to update a column(meets_requirements) in a STI table based on some conditional business logic in typed sub-models. The inheritance relationship in our existing export models is determined by the type of entity they export (e.g. ProductExport, DigitalAssetExport, etc.). Our definition of minimum requirements has changed and we would like to update all of our product exports to ensure they are correctly flagged. Please note that the following example has been simplified to illustrate a point and is not intended to exemplify the best method for achieving the following type of migration:

So far it looks as if we are following migration best practices by redefining any models we attempt to use, performing a single atomic transaction, etc. However, when this migration was run, it failed! The error message read:

How can this be? The method meets_all_requirements? is defined on the ProductExport model. However, you may notice that the error message is telling us exactly what is happening. Our migration is running with an instance of ProductExport, not MarkProductExportsWhichMeetAllRequirements::ProductExport. Hmm, that is strange. In an effort to quickly resolve the issue we try to push all of the logic down into the Export model and use a where clause to fetch ProductExports:

Once again, we are greeted with the NoMethodError exception. Hmm. After a bit of head scratching, we realize that Rails is using it’s STI magic to instantiate an instance of ProductExport, stealthily thwarting our attempt to use our redefined models. The solution is simple, we need to explicitly tell Rails to ignore the inheritance column and instantiate an instance of our redefined Export model.

Voilà! Just like that, the migration is successful and everyone is happy. A simple change that may not seem obvious at first, but makes a lot of sense once you think about it. This type of gotcha would have probably slipped through the cracks had we been referencing an existing method on ProductExport and we would have ended up with a migration that had an external model dependency. We thought we’d share this information to help you avoid running into similar issues when performing migrations of your own on STI models.

Thanks for following along on this journey through Active Record Migrations. We’d love to hear about any stories/solutions you have come across while performing similar tasks.

  • amolpujari

    You must be having your models(/app/models) named using same names as “Export”, and “ProductExport”. Wouldn’t they get loaded before execution of this method `up`? And then you are opening them over here in migration.

    • Randy Burkes

      Hey @amolpujari:disqus thanks for reading. Rails will actually look to use any classes defined in the scope of the migration before falling back on externally defined (/app/models) classes (This is no different than the standard runtime class resolution in ruby). This behavior is desirable in migrations, as referencing models defined in /app/models can be problematic in some cases.

      Furthermore, when querying tables using STI, rails will instantiate the class based of the value stored in the configured inheritance column. Since the value stored in the inheritance column is `ProductExport’ not `MigrateProductExportsWhichMeetAllRequirements::ProductExport’ our redefined class will never be used. Telling rails to ignore the inheritance_column for a table effectively forces it to behave as if it were not using STI, allowing the model defined in the migration class to be used.