Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker

merging scopes with STI models

JT Archie
Thursday, August 30, 2012

Introduction

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.

Models

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

Problem

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.

Solution

We can extend Person to return an array of each active FireFighter and PoliceOfficer.

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

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.

Advance Solution

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

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)
=> []
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

2 Comments

  1. javio says:

    I want to confirm that, your post is so interesting. It contains a lot of important and useful information. I got a lot of great things. Thank you so much!
    is it just me

    October 31, 2012 at 9:10 pm

  2. javio says:

    Hi, this is a very interesting article and I have enjoyed read­ing many of the arti­cles and posts con­tained on the web­site, keep up the good work and hope to read some more inter­est­ing con­tent in the future. I got a lot of useful and significant information. Thank you so much.
    down or just me

    October 31, 2012 at 9:15 pm

Add New Comment Cancel reply

Your email address will not be published.

JT Archie

JT Archie
New York

Recent Posts

  • [NYC][Standup] 09/27/12: Protect all your attributes
  • [Standup][NYC] 09.24.2012 Moral support for the pub and sub
  • What I’ve learned about RubyMotion
Subscribe to JT's Feed

Author Topics

agile (5)
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >