Add STI to a Legacy ActiveRecord Model
Recently, I need to add STI (Single Table Inheritance1) to a long-existing table.
This table already uses a column (token_type
) to distinguish between
two different types of data. And token_type
can only be either ios
(for IosDevice
) or android
(for AndroidDevice
).
I thought this would be a simple task but I could found few resources about how to do it. So I'll explain how I did this in this post.
Set inheritance_column
To change the column STI uses, we need to set inheritance_column
in
our Base Model like this:
class Device::Base < ActiveRecord::Base self.inheritance_column = 'token_type' end
This behavior is already documented in ActiveRecord
's documentation:
Defines the name of the table column which will store the class name on single-table inheritance situations.
The default inheritance column name is type, which means it's a reserved word inside Active Record. To be able to use single-table inheritance with another column name, or to use the column type in your own model for something else, you can set inheritance_column:
Hack How ActiveRecord Finds the Sub-classes
Then comes the most important part of our solution. We need to make
ActiveRecord
finds our sub-class based on the existing value
ios
->IosDevice
android
->AndroidDevice
After reading ActiveRecord
's source code, we need to follow these
steps:
Overwriting
self.find_sti_class
method in the base model:class Device::Base < ActiveRecord::Base def self.find_sti_class(type_name) super("Device::#{type_name.classify}Device") end end
- Originally,
find_sti_class
was expecting sometype_name
more like a class name (e.g.Device::AndroidDevice
) - Using our hacker, we can still find
Device::AndroidDevice
even if the type value isandroid
- Originally,
Setting
store_full_sti_class
tofalse
in sub-classes (Optional)class Device::AndroidDevice < Device::Base self.store_full_sti_class = false end
store_full_sti_class
is aclass_attribute
inActiveRecord::Inheritance
- Setting it to
false
means that we are not storing the full class name in the type column - But actually we don't need to do this because we've already
extend it to the full class name in
find_sti_class
Setting
sti_name
for sub-classesclass Device::AndroidDevice < Device::Base def self.sti_name 'android' end end
- This
sti_name
will be used to constructtype_condition
- Then
type_condition
will be used inActiveRecord::Core#relation
to add constraints oninheritance_column
when querying
- This
Caveats
There are some caveats for using this hack. (That's why it's called a hack and there are few resources introducing this.)
find_sti_class
is a private method- Which means it might be changed or even removed in the future
- This possibility makes our hack pretty unstable
- Fortunately, we are just calling super and giving it a different input.
- So that, as long as this method is not removed and it's still
used to find a class to initialize the
ActiveRecord
object, this hack will continue to work.
sti_name
is also a private method- It's more unstable than
find_sti_class
because it's used in a place far fromActiveRecord::Inheritance
and is more possible to be changed - More importantly, we are overwriting the whole method instead of
calling
super
, which means we will very likely break this code in a future upgrade
- It's more unstable than
token_type
is doing more than one thing- The reason I wanted to use
token_type
as theinheritance_column
was that we've already been using it to distinguishiOS
devices andAndroid
devices. - But still, after we add the responsibility of finding the STI class for this column. It's doing more than one thing.
- If we need to change the original values of
token_type
, it will affect the STI mechanism for these classes. - It's definitely better to use a separate
type
column for STI. (Or remove the old responsibility to make it only responsible for STI) - Again, fortunately, both
token_type
(ios
andandroid
) are both stable enough and won't be changed in any foreseeable future.
- The reason I wanted to use
So, think about these trade-offs before you try out my hack to STI. (I've made the decision to use this because I'm willing to take this risk for the convenience this quick STI hack brings to the project)