On a client project recently, we ran into a domain problem that didn’t fit into the ActiveRecord standard conventions. The following is the thought process taken to get to our solution, so it gets detailed in some areas.
ActiveRecord has a great feature called Single Table Inheritance. It allows a model to have multiple types while using a single database table for the storage. Those type abstractions can each have their own validations, override base functionality, and specific abstraction functionality.
If your model has ever been littered with
case statements checking if a
User is a guest, admin, etc., you should take a look at STI.
The project had a based model that had many types and each abstraction had a scope of
.active that defined what it meant to be active for that type.
class Person < ActiveRecord::Base; end class FireFighter < Person def self.active where(has_helmet: true) end end class PoliceOfficer < Person def self.active where(has_squad_car: true) end end
We needed to create an API endpoint that returned all active
Person instances. This would require us to iterate through each child of
Person and get all its current active members. Since we have
Person model let’s give it a concept of
.active that incorporates every active member of society in our domain.
We can extend
Person to return an array of each active
class Person < ActiveRecord::Base def self.active FireFighter.active.all + PoliceOfficer.active.all end end
One problem we have with this implementation is every time we add a new abstraction of
Person we have to add to
.active. Luckily, ActiveRecord STI comes with support for looking up a parent’s
class Person < ActiveRecord::Base def self.active active_people = descendants.map do |descendant| d.active.all end.flatten end end
This is pretty powerful. We can add
Astronaut and any active astronauts will automatically be in
Person.active array. This implementation will help satisfy our API endpoint requirements, but it does break useful ActiveRecord patterns.
WARNING: Continue at your own risk. If you are content with the solution above stop, but if you want to see what can be done with Arel continue.
What if we want to chain scopes or extend the
.active with pagination for our API? We cannot do this because easily because we are currently returning a Ruby array instead of an ActiveRecord::Relation. How can we modify
.active to be an actual scope?
You might be thinking, ActiveRecord comes with the ability to merge scopes between models. Unfortunately, it does not work very well when merging scopes with STI models.
We ended using Arel (known for not being well documented) within our model. Each ActiveRecord::Relation is actually just an object with holding on to Arel values for different parts of an SQL statement — joins, froms, selects, etc. We are able to get the conditions for
WHERE clause by looking at the ActiveRecord::Relation
class Person < ActiveRecord::Base def self.active conditions = descendants.map do |d| d.active.where_values.reduce(:and) end.reduce(:or) where(conditions) end end
Our implementation takes the
where_values from the
.active scope from each descendant and does an SQL
OR on them. ActiveRecord::Relation can take
# somewhere in a Rails console > Person.active.to_sql => SELECT "people".* FROM "people" WHERE ( ("people"."has_helmet" = 't' AND "people"."type" = "FireFighter") OR ("people"."has_squad_car" = 't' AND "people"."type" = "PoliceOfficer") )
What does give us? We can now use
Person.active as a normal scope, which allows us to append any conditions on to it.
> Person.active.where(created_at: 2.days.ago..1.day.ago).order(:created_at) =>  > Person.active.limit(10) => 
About the AuthorMore Content by JT Archie