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.