Flatiron Labs
Published in

Flatiron Labs

How to Remove Single Table Inheritance from Your Rails Monolith

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

About Single Table Inheritance (STI)

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

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

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

Dual Writes

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

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

  • The singular and plural forms of the model, including all of its subclasses, methods, utility methods, associations and queries.
  • Hardcoded SQL queries
  • Controllers
  • Serializers
  • Views
  • :content — for associations and queries
  • :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. Replace the method behavior in the definition or change the method at the call site
  2. Write new methods and call them behind a feature flag at the call site
  3. Break dependencies on associations with methods
  4. Raise errors behind a feature flag if you’re unsure about a method
  5. Swap in objects that have the same interface
Footer top
Footer bottom

--

--

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.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store