Single table inheritance (STI) in Rails allows you to store Ruby subclasses in the same database table.
Let’s get started with a brand-new Rails project to learn what that means. I’ll be using Rails 4.2.3 and SQLite as the database.
$ rails new sti-demo
First, an empty User model:
In app/models/user.rb
:
class User < ActiveRecord::Base
end
Generate a migration for it. To implement STI, we add a column called type
of type string
to the class. Let’s also have a name column:
$ rails g migration create_users type:string name:string
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :type
t.string :name
end
end
end
Migrate the SQLite database:
$ rake db:migrate
== 20150627182720 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0012s
== 20150627182720 CreateUsers: migrated (0.0012s) =============================
Fire up the Rails console:
$ rails c
Let’s create a new user:
>> User.create({name: "Mr. Bean"})
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "users" ("name") VALUES (?) [["name", "Mr. Bean"]]
(2.6ms) commit transaction
=> #<User id: 1, type: nil, name: "Mr. Bean">
No problems so far.
Now, let’s say we want to differentiate users between regular users and power users. At this juncture, let’s pretend we don’t know about STI. Since we’ve already created the type
column, let’s use that, so we can find different types of users by writing something like this:
>> User.where(type: "PowerUser")
Go ahead and create such a user:
>> User.create({name: "George", type: "PowerUser"})
ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: PowerUser is not a subclass of User
from /Users/siawyoung/.rvm/gems/ruby-2.2.0/gems/activerecord-4.2.1/lib/active_record/inheritance.rb:215:in `subclass_from_attributes'
from /Users/siawyoung/.rvm/gems/ruby-2.2.0/gems/activerecord-4.2.1/lib/active_record/inheritance.rb:55:in `new'
from /Users/siawyoung/.rvm/gems/ruby-2.2.0/gems/activerecord-4.2.1/lib/active_record/persistence.rb:33:in `create'
from (irb):8
Uh oh. What just happened? Why is Active Record complaining about PowerUser
not being a subclass of User
?
It turns out that Active Record, upon, seeing a column named type
, automatically assumes you’re implementing STI.
#Into Rails
Down The Stack
Let’s take a dive into the Rails source code to find out how Active Record automatically assumes this behaviour, by finding out step by step what happens when we attempt to type User.create({name: "George", type: "PowerUser"})
to create a new power user.
The source code below is as of commit 943beb1ea23866d46922a5c850f79a0fb9531f9b
on the 4-2-stable
branch.
Sean Griffin explains briefly in his comments in Active Record’s inheritance
module.
In activerecord/lib/active_record/inheritance.rb
:
# Active Record allows inheritance by storing the name of the class in a column that by
# default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
# This means that an inheritance looking like this:
#
# class Company < ActiveRecord::Base; end
# class Firm < Company; end
# class Client < Company; end
# class PriorityClient < Client; end
#
# When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
# the companies table with type = "Firm". You can then fetch this row again using
# <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
... (some comments about dirty checking) ...
# If you don't have a type column defined in your table, single-table inheritance won't
# be triggered. In that case, it'll work just like normal subclasses with no special magic
# for differentiating between them or reloading the right type with find.
create
method in activerecord/lib/active_record/persistence.rb
:
persistance.rb create
def create(attributes = nil, &block)
# attributes == {name: "George", type: "PowerUser"}
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
object = new(attributes, &block)
object.save
object
end
end
We start our investigation from Active Record’s persistance
module, which contains the method definitions of some of the most-commonly used Active Record methods, such create
, new
, save
, update
and destroy
(protip: use Command-R
in Sublime Text to search by method definition).
We first check if the supplied attributes
argument is an array. If so, we recursively call create
for each member in the array. This means you can do something like:
User.create([[{name: "a"}, {name: "b"}], {name: "c"}]) # notice the nested array
and Active Record will create three users, and even return them to you in the same array structure you specified:
(0.1ms) begin transaction
SQL (6.1ms) INSERT INTO "users" ("name") VALUES (?) [["name", "a"]]
(0.8ms) commit transaction
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "users" ("name") VALUES (?) [["name", "b"]]
(1.0ms) commit transaction
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "users" ("name") VALUES (?) [["name", "c"]]
(0.7ms) commit transaction
=> [[#<User id: 7, type: nil, name: "a">, #<User id: 8, type: nil, name: "b">], #<User id: 9, type: nil, name: "c">]
If we supply a hash, then the new
method is called:
new
method in activerecord/lib/active_record/inheritance.rb
:
persistance.rb create
inheritance.rb new
Notice the *args
splat operator, which converts all but the last &block
argument into an Array
:
def new(*args, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
# args == [{:name=>"George", :type=>"PowerUser"}]
attrs = args.first
if subclass_from_attributes?(attrs)
subclass = subclass_from_attributes(attrs)
end
if subclass
subclass.new(*args, &block)
else
super
end
end
We take the first member of the args
array and run the subclass_from_attributes?
method on it, which is located in the same file, some ways down:
subclass_from_attributes?
method in activerecord/lib/active_record/inheritance.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes?
def subclass_from_attributes?(attrs)
# attrs == {:name=>"George", :type=>"PowerUser"}
# attribute_names == ["id", "type", "name"]
# inheritance_column == "type"
attribute_names.include?(inheritance_column) && attrs.is_a?(Hash)
end
This method checks through all of the attributes in the model to see if any of their names match the specified inheritance column, which in this case is "type"
. Therefore, subclass_from_attributes?
returns true
.
Let’s see where attribute_names
and inheritance_column
come from.
attribute_names
method in activerecord/lib/active_record/attribute_methods.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes?
attribute_methods.rb attribute_names
# Returns an array of column names as strings if it's not an abstract class and
# table exists. Otherwise it returns an empty array.
#
# class Person < ActiveRecord::Base
# end
#
# Person.attribute_names
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
column_names
else
[]
end
end
where column_names
is:
column_names
method in activerecord/lib/active_record/model_schema.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes?
attribute_methods.rb attribute_names
model_schema.rb column_names
def column_names
@column_names ||= columns.map { |column| column.name }
end
I’m going to stop here because columns
steps into whole new territory: namely, into the scary ActiveRecord::ConnectionAdapters
module. What this means is that, its at this point where Rails actually connects to the database itself to get (and cache) information about its tables.
In fact, you can call it in the console and see for yourself:
>> User.columns
[#<ActiveRecord::ConnectionAdapters::Column:0x007fa29fa2b0e0
@cast_type=
#<ActiveRecord::Type::Integer:0x007fa298e34bc0
... (redacted) ...
@name="id",
@null=false,
@sql_type="INTEGER">,
#<ActiveRecord::ConnectionAdapters::Column:0x007fa29fa2afc8
@cast_type=
#<ActiveRecord::Type::String:0x007fa29fa2b680
... (redacted) ...
@name="type",
@null=true,
@sql_type="varchar">,
#<ActiveRecord::ConnectionAdapters::Column:0x007fa29fa2aeb0
@cast_type=
#<ActiveRecord::Type::String:0x007fa29fa2b680
... (redacted) ...
@name="name",
@null=true,
@sql_type="varchar">]
And finally, the crux of the matter, which can be found in activerecord/lib/active_record/model_schema.rb
:
inheritance_column
method in activerecord/lib/active_record/model_schema.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes?
model_schema.rb inheritance_column
# 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+:
#
# self.inheritance_column = 'zoink'
def inheritance_column
(@inheritance_column ||= nil) || superclass.inheritance_column
end
# Sets the value of inheritance_column
def inheritance_column=(value)
@inheritance_column = value.to_s
@explicit_inheritance_column = true
end
As you can see, I’ve included both the getter and setter for inheritance_column
. So this is the setter that allows you to do self.inheritance_column = :some_other_column_name
to change the name of the inheritance column that Rails looks for, in case you don’t want to use "type"
.
Where in the code is "type"
explicitly defined as a default though?
module ModelSchema
extend ActiveSupport::Concern
included do
... (redacted) ...
self.inheritance_column = 'type'
end
A-ha! It’s included as a concern in the included
block.
Up The Stack
Let’s pick up where we left off, at new
:
persistance.rb create
inheritance.rb new
if subclass_from_attributes?(attrs)
subclass = subclass_from_attributes(attrs)
end
Since one of the attributes does include the specified inheritance column, subclass_from_attributes?
returns true
, and so Rails runs the subclass_from_attributes
method, found in the same file:
subclass_from_attributes
method in activerecord/lib/active_record/inheritance.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes
def subclass_from_attributes(attrs)
# attrs == {:name=>"George", :type=>"PowerUser"}
subclass_name = attrs.with_indifferent_access[inheritance_column]
# subclass_name == "PowerUser"
if subclass_name.present?
subclass = find_sti_class(subclass_name)
if subclass.name != self.name
unless descendants.include?(subclass)
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}")
end
subclass
end
end
end
This method takes the value in the attribute identified as the inheritance column, then attempts to find a subclass by that name, using the find_sti_class
method.
with_indifferent_access
allows you to access a hash with either a symbol or string representation of the key:
>> attrs
=> {:name=>"George", :type=>"PowerUser"}
>> attrs[:type]
=> "PowerUser"
>> attrs["type"]
=> nil
>> attrs.with_indifferent_access[:type]
=> "PowerUser"
>> attrs.with_indifferent_access["type"]
=> "PowerUser"
find_sti_class
method in activerecord/lib/active_record/inheritance.rb
:
persistance.rb create
inheritance.rb new
inheritance.rb subclass_from_attributes
inheritance.rb find_sti_class
def find_sti_class(type_name)
# type_name == "PowerUser"
if store_full_sti_class # defaults to true
ActiveSupport::Dependencies.constantize(type_name)
else
compute_type(type_name)
end
rescue NameError
raise SubclassNotFound,
"The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
"or overwrite #{name}.inheritance_column to use another column for that information."
end
store_full_sti_class
defaults to true, unless it is explicitly set to false in the model.
Then, we use the constantize
method provided courtesy of the Dependencies module in Active Support to find out if such a class has indeed been loaded by Rails during initialization:
>> ActiveSupport::Dependencies.constantize("User")
=> User(id: integer, type: string, name: string)
>> ActiveSupport::Dependencies.constantize("PowerUser")
*** NameError Exception: wrong constant name PowerUser
=> nil
And of course, constantize
returns an exception because there’s no PowerUser or PowerUser class defined, let alone loaded. This NameError exception is rescued by find_sti_class
, which raises the exception which we witnessed near the start of this post.
#Patching Things Up
Let’s include a class definition for PowerUser
:
class User < ActiveRecord::Base
end
class PowerUser < User
end
This time:
>> ActiveSupport::Dependencies.constantize("User")
=> User(id: integer, type: string, name: string)
>> ActiveSupport::Dependencies.constantize("PowerUser")
=> PowerUser(id: integer, type: string, name: string)
and then we go into the next if
clause in subclass_from_attributes
:
# subclass.name == "PowerUser"
# self.name == "User"
if subclass.name != self.name
unless descendants.include?(subclass)
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}")
end
subclass
end
where descendants
is a method in Active Support’s DescendantsTracker module which is used to keep track of the Rails object hierarchy and descendants in memory. The module exposes two methods: descendants
, and direct_descendants
, both of which should be self-explanatory.
>> User.descendants
=> [PowerUser(id: integer, type: string, name: string)]
>> User.direct_descendants
=> [PowerUser(id: integer, type: string, name: string)]
Now, since subclass
is now defined, new
is once again called with the subclass, and the cycle repeats.
if subclass
subclass.new(*args, &block)
else
And we’re finally done!