Sharing Query Logic Within ActiveRecord Models

Chris Toomey

I was recently working on an application in which we had a class method / scope used to return a specific set of users. This was fine and good until we found ourselves needing to capture the same logic at the instance level. Ideally, I was hoping to be able to avoid duplicating the logic, and simultaneously keep things efficient.

Users in our application can be configured to receive alerts. We have a class method on the User model used to query for all the users who can receive alerts:

class User < ApplicationRecord
  def self.can_receive_alerts
    where(receives_sms_alerts: true).
      or(where(receives_email_alerts: true)).
      joins(:alert_configurations).
      distinct
  end
end

In words, the criteria for can_receive_alerts is that a user has either email or sms alerts enabled, and at least one AlertConfiguration defined. This method will result in roughly the following SQL query:

SELECT * FROM users
  INNER JOIN alert_configurations ON alert_configurations.user_id = users.id
  WHERE (users.receives_sms_alerts = 't' OR users.receives_email_alerts = 't');

We’d like to be able to reuse this same query logic at the instance level, such that we can answer user.can_receive_alerts? for a specific model instance, ideally without having to duplicate the query logic. Thankfully, we can take advantage of ActiveRecord’s lazy query evaluation and build on the relation returned from the class method:

class User < ApplicationRecord
  # our class method from above
  def self.can_receive_alerts
    where(receives_sms_alerts: true).
      or(where(receives_email_alerts: true)).
      joins(:alert_configurations).
      distinct
  end

  # our new instance method which builds on the class method
  def can_receive_alerts?
    self.class.can_receive_alerts.where(id: id).exists?
  end
end

This new instance method, can_receive_alerts?, directly reuses the query logic from the class method, scoping it down to the user instance in question by chaining where(id: id), and then using ActiveRecord’s exists? method to produce an optimally efficient SQL query:

SELECT 1 AS one FROM users
  INNER JOIN alert_configurations ON alert_configurations.user_id = users.id
  WHERE (users.receives_sms_alerts = 't' OR users.receives_email_alerts = 't')
  AND users.id = 1
  LIMIT 1;

In the past, I likely would have either duplicated the logic at the instance level (and inevitably had them fall out of sync), or possibly used Ruby to scan the full set of users returned by the scope. Instead, we have a precise query which will only return the absolute minimum data needed, while directly reusing the query logic from the scope.

My focus is on ensuring that the core logic around how our application defines if a user can receive alerts is defined in only a single, canonical spot, but the performance niceties that come along are a wonderful side effect.