STI, Polymorphism and Abstract Classes — Rails

Abstract Base Classes

An abstract base class in Rails Model is simply a model that is not persistent, i.e not backed by a table. It would look like:

# app/models/citizen.rbclass Citizen < ApplicationRecord
self.abstract_class = true
end
# app/models/citizen.rbclass Citizen < ApplicationRecord
self.abstract_class = true
def fullname
"#{first_name} #{last_name}"
end
def eligible?
age >= 18
end
end
# app/models/electorate.rbclass Electorate < Citizen
...
end
# app/models/candidate.rbclass Candidate < Citizen
...
end

Single Table Inheritance (STI)

Sometimes, you have models that share some common attributes but also have a few different ones. STI is one of Rails’ provisions for such situations.

# migration file to add field type to citizendef change
add_column :citizens, :type, :string
end
# app/models/citizen.rbclass Citizen < ApplicationRecord
self.abstract_class = true # remove this line for STI
...
end
# app/models/electorate.rbclass Electorate < Citizen
...
end
# app/models/candidate.rbclass Candidate < Citizen
...
end
Example>> c = Candidate.create
>> c.type
=> "Candidate"
>> Citizen.first
=> #<Candidate:0x231456...>

Polymorphic Associations

There are situations where you have a model that belongs_to more than one other model. Rails provides polymorphic associations for this use case, where the belonging model has an association name that by convention has an able postfix. This model should have two fields that describe the id and type(class) of the associating record— ending in _id and _type. The association can be seen as an interface that this model exposes to other models. Let’s take a look at a Vote model for our citizen example.

# migration file for votesdef change
create_table :votes do |t|
...
t.references :votable, polymorphic: true, index: true
...
end
end
# app/models/vote.rbclass Vote < ApplicationRecord
belongs_to :votable, polymorphic: true
...
end
# app/models/electorate.rbclass Electorate < ApplicationRecord
has_many :votes, as: :votable
...
end
# app/models/candidate.rbclass Candidate < ApplicationRecord
has_many :votes, as: :votable
...
end
Example>> c = Candidate.create
>> c.votes
>> [#<Vote:0x0003437fdd79a91d0 id:1, votable_type: "Candidate", votable_id: 1...>]
>> v = Vote.first
>> v.votable
>> #<Candidate:0x02332fed3903e id:1, ...>
>> Candidate.find(50)
>> ActiveRecord::RecordNotFound: Couldn't find Candidate with 'id'=50...
>> Vote.create!(votable_id: 50, votable_type: "Candidate")
>> # Candidate Load (0.3ms) SELECT "candidates".* FROM "candidates" WHERE "candidates"."id" = $1 LIMIT $2 [["id", 50], ["LIMIT", 1]]
>> ActiveRecord::RecordInvalid: Validation failed: Votable must exist
SQL Shell
>> INSERT INTO votes (votable_id, votable_type, created_at, updated_at) VALUES (50, 'Candidate', '02-05-2019', '02-05-19');
# The above query succeeds even though a candidate with that id doesn't exist.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store