Rails concerns are a really good way to provide shared functionalities into your models. For example you can add scopes and then apply them in multiple classes:
module Statistics
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
end
class Host
include Statistics
end
One of the downsides of this solution is a "models pollution". Every model in which we want to include our module, will get all the methods and scopes from that module. With multiple concerns and extensions, it can create issues with methods and scopes overwriting each other. But as always with Ruby, there's a quite nice solution that will allow you to define custom scopes that will be encapsulated in a namespace method. This approach allows to have multiple scopes with the same name that will vary depending on an invocation context:
module Statistics
extend ActiveSupport::Concern
included do
# Some magic here...
scope :last_week, -> { where('created_at >= ?', 1.week.ago) }
end
end
module Calculable
extend ActiveSupport::Concern
included do
# Some magic here...
scope :last_week, -> { unscoped.where('created_at >= ?', 1.week.ago) }
end
end
class Host
include Statistics
include Calculable
end
# last_week accessible via statistics namespace
Host.statistics.last_week #=> all the hosts created last week
# last_week not available directly on a host class
Host.last_week #=> This will raise NoMethodError: undefined method last_week
# last_week should be also chainable with other scopes
Host.enabled.statistics.last_week #=> all enabled hosts from last week
# And the last_week method but from the calculable namespace
Host.calculable.last_week
Proxy object and instance_exec as a solution
To obtain such a behavior we can use a proxy object in our concern module:
module Statisticable
extend ActiveSupport::Concern
# Class that acts as a simple proxy for all statistical scopes
# We have it, because we don't want to "pollute" original klass
# that we extend with this. This way we can define scopes here
# and then access them via .statistics class method of a class
class Proxy
# @param [Symbol] name of a scope or a method
# @param [Block] block with a scope that will be binded to the class
def self.scope(name, block)
define_method(name) do |*args|
@scope.instance_exec(*args, &block)
end
end
# @param scope [ActiveRecord::Base] active record class for which we want to
# create statistics proxy
def initialize(scope)
@scope = scope
# Here we could also use any scope that we want, like:
# @scope = scope.unscoped
end
%i( week month year ).each do |time|
# @return [Integer] number of elements that were created in a certain time
scope :"last_#{time}", -> { where('created_at >= ?', 1.send(time).ago) }
end
# We can add any standard method as well - but it can't be chained further
scope :summed, -> { sum(:id) }
end
included do
# @return [Statisticable::Proxy] proxy that provides us with scopes for statistical info
# about a given class
# @example Get statistics proxy for Like class
# Like.statistics #=> Statisticable::Proxy instance
def self.statistics
@statistics ||= Statisticable::Proxy.new(self)
end
end
end
It uses the instance_exec method to execute all the scopes in our base class (@scope) context.
Now in every AR in which we include this Statisticable concern, all its scopes and methods will be available via statistics namespace.