How to Remove Single Table Inheritance from Your Rails Monolith

Inheritance is easy — until you have to deal with technical debt and taxes.

Tracy Lum
Tracy Lum
Dec 12, 2018 · 10 min read

About Single Table Inheritance (STI)

In brief, Single Table Inheritance in Rails allows you to store multiple types of classes in the same table. In Active Record, the class name is stored as the type in the table. For example, you might have a Lab, Readme, and Project all live in the contents table:

class Lab < Content; end
class Readme < Content; end
class Project < Content; end
create_table "content", force: :cascade do |t|
t.integer "curriculum_id",
t.string "type",
t.text "markdown_format",
t.string "title",
t.integer "track_id",
t.integer "github_repository_id"
end

Identifying the Scope of Work

Content sprawled throughout the app, sometimes confusingly. For example, this described the relationships in the Lesson model.

class Lesson < Curriculum
has_many :contents, -> { order(ordinal: :asc) }
has_one :content, foreign_key: :curriculum_id
has_many :readmes, foreign_key: :curriculum_id
has_one :lab, foreign_key: :curriculum_id
has_one :readme, foreign_key: :curriculum_id
has_many :assigned_repos, through: :contents
end
class Content < ActiveRecord::Base
belongs_to: :lesson, foreign_key: :curriculum_id
belongs_to: :github_repository
end
class AssignedRepo < ActiveRecord::Base
belongs_to :content
belongs_to :readme
belongs_to :lab
belongs_to :project
end
Old to New System Diagram, where red dotted lines indicate paths marked for deprecation

The key takeaway though is that we had to replace a model in a pretty big codebase, and ended up changing somewhere in the realm of 6000 lines of code.

Strategies for Refactoring and Replacing STI

The New Model

First, we created a new table called canonical_materials and created the new model and associations.

class CanonicalMaterial < ActiveRecord::Base
belongs_to :github_repository
has_many :lessons
end

Dual Writes

After the new tables and columns were in place, we started writing to the old tables and the new ones simultaneously so that we wouldn’t need to run a backfill task more than once. Any time something tried to create or update a content row, we’d also create or update a canonical_material.

lesson.build_content(
'repo_name' => repo.name,
'github_repository_id' => repo_id,
'markdown_format' => repo.readme
)

lesson.canonical_material = repo.canonical_material
lesson.save

Backfilling

The next step in the process was to backfill the data. We wrote rake tasks to populate our tables and ensure that a CanonicalMaterial existed for each GithubRepository and that each Lesson had a CanonicalMaterial. And then we ran the tasks on our production server.

In our experience, it’s been more confusing and costly to maintain code that supports legacy thinking than it has been to backfill and make sure the data is valid.

Replacement

And then the fun part began. In order to make the replacement as safe as possible, we used feature flags to ship dark code in smaller PRs, which enabled us to create a faster feedback loop and know sooner if things were breaking. We used the rollout gem, which we also use for standard feature development, to do this.

  • Hardcoded SQL queries
  • Controllers
  • Serializers
  • Views
  • :contents — for associations and queries
  • .joins(:contents) — for join queries, which should be caught by the previous search
  • .includes(:contents) — for eager loading second-order associations, which should also be caught by the previous search
  • content: — for nested queries
  • contents: — again, more nested queries
  • content_id —for queries directly by id
  • .content — method calls
  • .contents — collection method calls
  • .build_content — utility method added by the has_one and belongs_to association
  • .create_content — utility method added by the has_one and belongs_to association
  • .content_ids — utility method added by the has_many association
  • Content — the class name itself
  • contents — the plain string for any hardcoded references or SQL queries
  1. Write new methods and call them behind a feature flag at the call site
  2. Break dependencies on associations with methods
  3. Raise errors behind a feature flag if you’re unsure about a method
  4. Swap in objects that have the same interface


Footer top
Footer bottom

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork company). Together, we're building a global campus for lifelong learners focused on positive impact.

Tracy Lum

Written by

Tracy Lum

Software Engineer @flatironschool building learn.co.

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork company). Together, we're building a global campus for lifelong learners focused on positive impact.