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.
October 5, 2017 — 05:50
This is excellent, thank you sharing. I’m going to make sure I stop polluting from now on. I can’t believe I only just found this. I’m going to share it on Twitter, everyone should know this.