Search code examples
ruby-on-railsnamed-scopeactivesupport-concern

Rails: How to override or make private a given scope from an included Concern?


I have a concern that adds a scope to ActiveRecord classes that include it. Most of the time its fine the way it is but depending on the class there might extra criteria that needs to be met. I don't want to have to rewrite the scope in the model but be able to just add on an extra criteria method.

Here's what I'm trying to do:

module MyConcern
  extend ActiveSupport::Concern

  included do
    # I've included the fact I pass a limit variable here because my scope also does this, in case that's relevant for solving this.
    # I'd like this to be private or not accessible via `MyModel._my_scope`
    scope :_my_scope, ->(limit = nil) { (joins, wheres, order etc etc).limit(limit) }
    scope :my_scope, ->(limit = nil) { _my_scope(limit) }
  end
end

class MyModel < ApplicationRecord
  include MyConcern

  # Including the "private" scope to add my extra criteria.
  scope :my_scope, ->(limit = nil) { _my_scope(limit).where(foo: 'bar') }
end

class AnotherModel < ApplicationRecord
  include MyConcern

  # I like the default behavior here so nothing else to do
end

This works but from outside the class you can do this: MyModel._my_scope which I guess is ok - maybe theres a time when I'd want the default behavior - but in this case I dont think I do and I feel like encapsulating _my_scope is the way to go.

I'm assuming it's possible to make a private class method in MyConcern that gets included in MyModel but that doesn't seem to work and I'm not really a Concern/mixin master yet so not sure how to go about doing this. Also, are Concerns considered to be mixins? Is there a difference? That would be good to know also.


Solution

  • You can achieve the same functionality of scopes with class methods, that you can inherit and extend for this case. It's not much different than your implementation; just avoids the use of the extra _ method by using a class method instead of a scope. E.g.

    module MyConcern
      extend ActiveSupport::Concern
    
      class_methods do
        def my_scope(limit = nil)
          (joins, wheres, order etc etc).limit(limit)
        end
      end
    end
    
    class MyModel < ApplicationRecord
      include MyConcern
    
      def self.my_scope(limit = nil)
        super.where(foo: 'bar')
      end
    end
    
    class AnotherModel < ApplicationRecord
      include MyConcern
    end
    
    

    For the second part of your question: A concern is technically a ruby Mixin; it's just a convention to organize/group the Mixins that are included in your Models only as concerns. Using ActiveSupport::Concern allows you to add additional Model related functionality to your Mixins like scope, validations etc. which you can't get using a normal Module. E.g. You can't do

    module MyConcern
      scope :my_scope, ->(limit = nil) { _my_scope(limit) }
    end
    
    class MyModel < ApplicationRecord
      include MyConcern # this will raise an error
    end