STI store: Putting attributes in their place
The problem
You have a model which uses single table inheritance, and you need to add an attribute which exists for some sub-classes, but not others.
Firstly, it's worth noting that using polymorphic associations may be a better fit for your use case, but if you're sticking with STI, read on. For the remainder of the post, we'll implement Martin Fowlers's example class hierarchy.
A Rails app has been created to demonstrate the approaches below, which can be found here. For our starting point we've already added the Player
model, which uses STI to define two sub-classes: Cricketer
and Footballer
.
The 'extra column' solution
We'd like to add batting average, but only for the Cricketer
model. Normally in this situation, we're stuck adding the column to the players
table:
This has a few disadvantages, though. For example, both the Player
and Footballer
models will now respond to the batting_average
accessor methods, even though they shouldn't:
1
2
3
4
2.1.2 :001 > Player.first
=> #<Footballer id: 1, name: "Alice", created_at: "2015-04-18
2.1.2 :002 > Player.first.batting_average
=> nil
Also, whenever we have a non-cricketer, they'll always have a null batting_average
:
1
2
3
4
5
6
sti_store_development=# SELECT name, type, batting_average FROM players;
name | type | batting_average
-------+------------+-----------------
Alice | Footballer |
Bob | Cricketer | 61.2
(2 rows)
This isn't so bad for one column, but following on with the example and adding bowler model it's easy to see how this becomes compounded as we add additional sub-class specific attributes:
1
2
3
4
5
6
7
sti_store_development=# SELECT name, type, batting_average, bowling_average FROM players;
name | type | batting_average | bowling_average
-------+------------+-----------------+-----------------
Alice | Footballer | |
Bob | Cricketer | 61.2 |
Carol | Bowler | 12.3 | 80.3
(3 rows)
An implementation of this this solution may be found here.
The 'STI store' solution
For our new approach we will use the lesser known store_accessor
method from ActiveRecord::Store
, let's see how we can use it to add a batting_average
only to the Cricketer
model:
Firstly, instead of adding a separate column for each sub-class specific attribute, we'll add a single column to players
called sti_store
, give it type json
:
Now we can use the store_accessor
to generate accessor methods for batting_average
. Crucially, we'll specify this on the Cricketer
sub-class:
1
2
3
4
class Cricketer < Player
store_accessor :sti_store, :batting_average
…
end
By doing this we have a solution to the first issue given above: The Cricketer
model responds to the batting_average
accessor, but Footballer
and Player
do not.
At this point it'd be nice to add a validation in the sub-class for our new attribute, and it works with store_accessor
as expected:
1
2
3
4
5
class Cricketer < Player
store_accessor :sti_store, :batting_average
validates :batting_average, numericality: { less_than_or_equal_to: 100 }
…
end
Following the example, let's add the Bowler
model, as a sub-class of Cricketer
, again we just specify its bowling_average
attribute with store_accessor
:
1
2
3
4
class Bowler < Cricketer
store_accessor :sti_store, :bowling_average
…
end
Note that the Bowler
model inherits its parent's batting_average
attribute, as shown here.
Looking at this in psql
we now see the single sti_store
column, showing only the appropriate attributes for each type
:
1
2
3
4
5
6
7
sti_store_development=# SELECT name, type, sti_store FROM players;
name | type | sti_store
-------+------------+-------------------------------------------------
Alice | Footballer |
Bob | Cricketer | {"batting_average":61.2}
Carol | Bowler | {"batting_average":12.3,"bowling_average":80.3}
(3 rows)
Much better.
Errata:
Is PostgreSQL's JSON type required?
Not necessarily, we use store_accessor
directly, but it should be
possible to use store
instead (see the ActiveRecord::Store
documentation).
Need help with your project?
We specialize in Ruby on Rails and JavaScript projects. Code audits, maintenance and feature development on existing apps, or new application development. We've got you covered.