STI, Polymorphism and Abstract Classes — Rails

Olaide Ojewale
4 min readFeb 6, 2019

--

There are times where certain models need to share some behavior but their identities differ. Rails provides some baked-in functionality to handle these situations, we’d be looking at 3 of them.

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

Declaring the model as abstract tells Rails to see it as one that’s not persistent and would be used to share functionality with its subclasses via Inheritance.

Say we have two other models that represent different categories of citizens: electorate and candidate. These models can share some properties like fullname and eligible.

Assuming that in our country of choice the minimum age to engage in electoral activities is 18years. Then the models could look like:

# 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

As you rightly enthused, the candidate and electorate models both have age, first_name and last_name fields. Now, they both can make use of the methods defined in the Citizen model. Class & instance methods, constants and other class members that might be brought in through module inclusion are passed down to the subclasses in this inheritance hierarchy, however, it’s advisable to not turn the abstract base class into a dumping ground in the guise of shared functionality.

It’s pertinent to note that in this setup, citizen has no underlying table but electorate and candidate do have underlying tables.

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.

In an STI setup, you have a model that is parent(or super) to other models. This parent model must contain a field named type with no default value needed. The type field automatically stores the name of the child model(subclass) to which the record belongs. Taking our citizen example:

# 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...>

In this case, the electorate and candidate models do not need to have underlying tables. The only table needed is the citizens table. As seen in the example above, for all subclasses, Rails automatically figures out the model to which a record belongs.

Since all subclasses share the same table, you cannot have the same attribute on two subclasses with different datatypes. As the STI table gets bigger and bigger, it might have too many null fields. Fields that exist only on a subclass would be null for other subclasses. There are other pros and cons of STIs but that could be a discussion for another time.

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, ...>

The association works on both — the belongs_to and the has_many — sides, taking advantage of the id and the type columns.

Rails and Active Record provide some security by ensuring that the type and id of the record saved on a polymorphic model represent an actual record that belongs to this relationship chain. However, if someone has access to your database, they can create orphan records because polymorphic associations don’t have the foreign key constraint of a typical belongs_to association.

>> 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.

Sidenote: It’s possible to have polymorphic associations on STIs but I decided to keep the example focused on simple polymorphic associations. Feel free to look into that if you’re curious 😉

While designing your data models, you’ll probably figure out when it is appropriate to use each of these. Eventually, choosing the right strategy could be as important as solving the problem.

--

--

Responses (1)