Single table inheritance (often referenced to as STI) is a feature offered by Rails’s ActiveRecord (which is documented here) that allows you to subclass models to store data for multiple similar models in one database table. This can be a very useful feature under the right circumstances.
When working with Rails in development mode classes are not preloaded (unlike is done in production mode). If you use the descendants method to get a list of the subtypes of your model (to for example display them or to populate a select box) it is possible that sometimes an empty or partial list is returned rather than a list of all the defined subtypes (unless you have actually used one of them so far).
If you are working in a team this can be an especially devious little problem that can linger for a long time. Different developers may have different sets of data in their development system. Loading a list of STI models will load the subtype classes making them available in the application from that point on. If the database however does not contain records for the subtype’s class and you are not directly referencing it you will not see it. This can then result in one developer experiencing issues while the another does not.
In this article I will demonstrate the issue and offer a solution that you can implement.
The example
Let’s say that our application supports storing notes of two types: to-do and recipe. This has been implemented using a Note model that has two subtypes: Notes::Todo and Notes::Recipe.
If you go into the rails console in production mode you can see the subtypes using the descendants method:
vagrant@muridae:~/code/apps/mr-data (trunk) $ RAILS_ENV=production bin/rails c
Loading production environment (Rails 6.0.3.4)
[1] pry(main)> Note.descendants
=> [Notes::Todo (call 'Notes::Todo.connection' to establish a connection),
Notes::Recipe (call 'Notes::Recipe.connection' to establish a connection)]
When accessing the rails console in development mode you will however get an empty list:
vagrant@muridae:~/code/apps/mr-data (trunk) $ bin/rails c
Loading development environment (Rails 6.0.3.4)
[1] pry(main)> Note.descendants
=> []
If you access one of the subtypes they will from that point an appear in the list:
[2] pry(main)> Notes::Todo
=> Notes::Todo (call 'Notes::Todo.connection' to establish a connection)
[3] pry(main)> Note.descendants
=> [Notes::Todo (call 'Notes::Todo.connection' to establish a connection)]
How to fix it
When searching around you will find various ways in which this can be fixed, the simplest way that I could find is to ensure that the subtypes are loaded when the model itself is loaded by using require_dependency:
class Note < ApplicationRecord
end
require_dependency 'notes/todo'
require_dependency 'notes/recipe'
The above will load the Notes::Todo and Notes::Recipe subtypes models from app/models/notes when the Note model is loaded.
If you now restart the rails console you will see that the descendants method now returns the subtypes properly:
vagrant@muridae:~/code/apps/mr-data (trunk) $ bin/rails c
Loading development environment (Rails 6.0.3.4)
[1] pry(main)> Note.descendants
=> [Notes::Todo (call 'Notes::Todo.connection' to establish a connection),
Notes::Recipe (call 'Notes::Recipe.connection' to establish a connection)]