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->IosDeviceandroid->AndroidDevice
After reading ActiveRecord's source code, we need to follow these
steps:
Overwriting
self.find_sti_classmethod 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_classwas expecting sometype_namemore like a class name (e.g.Device::AndroidDevice) - Using our hacker, we can still find
Device::AndroidDeviceeven if the type value isandroid
- Originally,
Setting
store_full_sti_classtofalsein sub-classes (Optional)class Device::AndroidDevice < Device::Base self.store_full_sti_class = false end
store_full_sti_classis aclass_attributeinActiveRecord::Inheritance- Setting it to
falsemeans 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_namefor sub-classesclass Device::AndroidDevice < Device::Base def self.sti_name 'android' end end- This
sti_namewill be used to constructtype_condition - Then
type_conditionwill be used inActiveRecord::Core#relationto add constraints oninheritance_columnwhen 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_classis 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
ActiveRecordobject, this hack will continue to work.
sti_nameis also a private method- It's more unstable than
find_sti_classbecause it's used in a place far fromActiveRecord::Inheritanceand 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_typeis doing more than one thing- The reason I wanted to use
token_typeas theinheritance_columnwas that we've already been using it to distinguishiOSdevices andAndroiddevices. - 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
typecolumn for STI. (Or remove the old responsibility to make it only responsible for STI) - Again, fortunately, both
token_type(iosandandroid) 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)