Single Table Inheritance using Rails 5.02

A few weeks ago I came across the need to implement Single Table Inheritance (STI) in my Rails backend API. Googling around, I found a few blog posts and a few stackoverflow threads that referred to STI, but they were all written a few years previously. If you, like me, start sweating when reading blog posts about code from a few months ago, never mind years, and you’re curious about STI, you may be relieved to find out that, as far as I know, not much if anything has changed with STI in Rails (i.e. those other posts are still relevant).

For the past few months I’ve been working on a political app where users can look at details about the legislators that they follow. As you might expect, I had a Legislator model with a single table and a single legislators_controller. This was all fine and good until I started working on a new requirement to include city and state legislators in addition to the ‘national’ ones (Congress).

At this point I had to make a design decision on my data structure. City and state legislators were very similar to the legislators I already had in my app so creating a new model / table and controller for each didn’t seem like it would be DRY. I considered a polymorphic association but discarded that idea because I would still need multiple models / tables (not DRY) and because writing something like:

class NationalLegislator
belongs_to :legislator, as: :legislatorable
end

seemed too funky. What’s a legislatorable anyway? At this point, Single Table Inheritance seemed like the best way to go due to its simplicity.

How does Single Table Inheritance Work?

In order to explain this, I’m going to borrow the heroes of Baltimore, Mr. and Prof. Trash Wheel.

In Practical Object-Oriented Design in Ruby: An Agile Primer Sandi Metz wrote,

“This the exact problem that inheritance solves; that of highly related types that share common behavior but differ along some dimension.”

Other than doing some serious foreshadowing with the word ‘type’ (more on that later), Ms. Metz is laying out the criteria we need to meet in order to implement STI.

  1. Our sub-classes / sub-models must be “highly related” to each other and our base-class / base-model.
  2. They must “share common behavior but differ along some dimension.” With STI in Rails I think of it as the other way around as in, ‘share dimensions (model attributes) but with slightly different behavior (methods in your model)’.

With these two rules in mind, let’s take a look at our trash wheels.

We can see from the above graphic that both Mr. Trash Wheel (left) and Professor Trash Wheel (center) are very similar to our top-level Trash Wheel. They all have white covers, they all have ramps for collecting trash, they all have water wheels and (disregard the base Trash Wheel for now), they all have googly eyes. Our right-hand image looks suspiciously like a trash wheel, it has googly eyes and is floating on the water, but has no wheel, no ramp and no white cover as far as we can tell. From the above graphic we can derive three classes / models.

# app/models/trash_wheel.rb
class TrashWheel < ApplicationRecord
# validations, constants and methods
end
# app/models/mr_trash_wheel.rb
class MrTrashWheel < TrashWheel
# validations, constants and methods
end
# app/models/professor_trash_wheel.rb
class ProfessorTrashWheel < TrashWheel
# validations, constants and methods
end

Our MrTrashWheel and ProfessorTrashWheel classes now share the same attributes as our base class / model TrashWheel and will also inherit any validations, constants and methods that we put in TrashWheel.

What value does Single Table Inheritance bring to my project?

Each of our sub-classes, however, can hold its own validations, constants and methods and this is really the greatest power of STI. Backing up to that Sandi Metz quote for a second, our models will not differ along attributes but they can differ in their behavior. For example, ProfessorTrashWheel is smart, she is a professor after all, and has a degree in trash studies with a focus on the Chesapeake Bay. If we create a new instance of ProfessorTrashWheel with ProfessorTrashWheel.new we’ll have access to the share_a_fact method which is not available to either our TrashWheel base class / model or the MrTrashWheel sub-class / sub-model.

On the flip side, each new instance of MrTrashWheel is initialized with snake: true and a call_snake method which we can use to say hi to Mr. Trash Wheel’s snek.

Finally, remember how this is Single Table Inheritance we’re talking about? This means that every new instance that we save to our database is stored in the same TrashWheel table. This means two things:

  1. Never create or save an instance of your class using BaseModel.create or BaseModel.save
  2. If you write a query using your BaseModel Rails will return all rows that match the query criteria but each instance will be identified by its sub-class. For example, TrashWheel.first might return<MrTrashWheel id: 1, eyes: ‘googly’, snake: true,… >

Okay cool, but how do I go about implementing STI in my own project? This is the e-z part.

Implementing Single Table Inheritance

  1. Add the type attribute to your BaseModel, for example: rails g migration add_type_to_model type:string The type column tells Rails that we’ll be using this model as our BaseModel / Base Class and that Rails should expect a sub-class / sub-model of a given type. Note: you can actually call the new column anything you want, but Rails by default uses the type column for STI (if you’ve ever tried to implement a type column for something that wasn’t STI, this is why you ran into errors). If we want to call our column sti_type instead, we would add self.inheritance_column = 'sti_type' inside of our base_model.rb file.
  2. Create our classes, i.e., this part.
# app/models/trash_wheel.rb
class TrashWheel < ApplicationRecord
# validations, constants and methods
end
# app/models/mr_trash_wheel.rb
class MrTrashWheel < TrashWheel
# validations, constants and methods
end
# app/models/professor_trash_wheel.rb
class ProfessorTrashWheel < TrashWheel
# validations, constants and methods
end

Note: In your config/environments/development.rb file Rails sets config.eager_load = false, what this means is that if you structure your files like so

File structure of models in Rails

you’ll need to add config.autoload_paths += %W(#{Rails.root}/app/models/trash_wheel) to your config/application.rb under class Application < Rails::Application

3. This isn’t really a required step but something to note if you’re using Rails in API mode and serving up JSON. Rails by default will not include the type attribute when rendering JSON. To do this you’ll need to either merge in type each time you render JSON or mutate the as_json method in your model.

# my_model.rb
def as_json(options={})
# as_json Coerces self to a hash for JSON encoding.
# https://apidock.com/rails/ActiveResource/Base/as_json
super(options.merge({ methods: :type }))
end

Congratulations! You’ve now set up Single Table Inheritance in your Rails app. Rails will now save all new records of the inherited classes to a single table, in this case called trash_wheel.

Issues with Single Table Inheritance

Single Table Inheritance revels in simplicity. It’s like a KISSing booth at a web developer carnival, don’t get carried away.

Right now we have two sub-classes / sub-models, MrTrashWheel and ProfessorTrashWheel. Should we implement STI with only two sub-classes? Suppose that we’re handed a new requirement, a third TrashWheel sub-class is on the way but we’re not sure what attributes it will have. Maybe this trash wheel will come with a remotely operated duck-catcher claw and use a powerful vacuum instead of a ramp to hoover up trash. Is it still a trash wheel? I can hear your trepidation, “kind of… ”

One big issue with STI is that it doesn’t scale well. New types / sub-classes will inevitably keep coming as new requirements are passed to us and our application keeps growing. With this in mind, it’s a good idea to only implement STI when you have three or more sub-classes / sub-models ready to go. If you only have two it may be worth it to make separate classes and tables until the the third one comes down the pipeline. It may not be the DRYest code, but it also won’t be as tricky to refactor out of.

Sub-models that both inherit from the same base-class and similar in most ways but different in others are, to put it mildly, contentious to Object Oriented Programmers. Because we’re using one table here, if we have a handful of sub-models that all vary slightly we’ll inevitably have 1). a massive table and 2). a lot of null values. While the first one just sounds scary (imagine a database with just one table, *shiver*), the second one isn’t that big a deal, but should be regarded as a best practice to avoid. It will also be difficult to scale re: the previous paragraph.

Because of these constraints I would think hard before implementing STI in a project, is it really the best way to structure your data for scale? If you’re unsure of where your sub-models could be heading (trash wheel with duck claw example) it may be best to defer implementing STI until you have a more clear view of where your project is heading.

Thanks for following along! You can find the trash wheel STI example at https://www.github.com/dcordz/trash_wheel_sti

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.