-1

Currently, the existing scopes are like this.

module TransactionScopes
      extend ActiveSupport::Concern
      included do
        scope :status, ->(status) { where status: status }
        scope :portfolio_id, ->(portfolio_id) { where portfolio_id: portfolio_id }
        scope :investor_external_reference_id, ->(investor_external_reference_id) { where investor_external_reference_id: investor_external_reference_id }
        scope :portfolio_external_reference_id, ->(portfolio_external_reference_id) { where portfolio_external_reference_id: portfolio_external_reference_id }
        scope :file_id, ->(file_id) { where back_office_file_id: file_id }
        scope :oms_status, ->(status) { where oms_status: status }
        scope :order_id, ->(order_id) { where order_id: order_id }
        scope :order_status, ->(order_status) { where order_status: order_status }
        scope :transaction_id, ->(transaction_id) { where transaction_id: transaction_id }
end

I have some more models with similar scopes, can I write more generic way so I can avoid these repeated processes.

  • 1
    Maybe you can. It's the usual process: identify the differences and extract them as input parameters. Sometimes, it's not worth it, because the end result becomes much less readable. – Sergio Tulentsev Jul 26 '21 at 12:42
  • Hey, @SergioTulentsev I want to write a generic class, which can be used in all the models to avoid writing scopes everywhere so that whenever a new attribute is added, we no need to add scope in the model every time. – Ramachandra Hegde Jul 26 '21 at 12:55

1 Answers1

4

I strongly discourage you from adding a scope for each single attribute. where(...) is only 5 characters and provides additional context to the reader. Person.where(name: 'John Doe') says: on Person execute a query (where) and return a collection that matches the criteria name: 'John Doe'.

If you add the suggest attribute scope the line becomes Person.name('John Doe'). By removing the context that this is a query a reader must "learn" that each attribute name can also be accessed as a scope.

The above immediately shows another issue, which is name conflicts. Person.name is already taken, and returns the class name. So adding scope :name, ->(name) { where(name: name) } will raise an ArgumentError.

Scopes can be useful, but when used too much they clutter the class method namespace of the model.

With the above out of the way, here are some actual solutions.


You could write a helper that allows you to easily create scopes for attributes. Then loop through the passed attributes and dynamically create scopes for them.

class ApplicationRecord < ActiveRecord::Base
  class << self

    private

    def attribute_scope_for(*attributes)
      attributes.each { |attr| scope attr, ->(value) { where(attr => value) } }
    end
  end
end

Then call this helper in your models.

class YourModel < ApplicationRecord
  attribute_scopes_for :status, :portfolio_id # ....
end

Alternatively, if you want to create a scope for each attribute you can dynamically collect them using attribute_names. Then loop through them and create scopes based on the names.

class ApplicationRecord < ActiveRecord::Base
  class << self

    private

    def enable_attribute_scopes
      attribute_names
        .reject { |attr| respond_to?(attr, true) }
        .each { |attr| scope attr, ->(value) { where(attr => value) } }
    end
  end
end

In the above snippet .reject { |attr| respond_to?(attr, true) } is optional, but prevents the creation of scopes that have a name conflict with a current public/private class method. This will skip those attributes. You can safely omit this line, but the scope method might raise an ArgumentError when passing dangerous scope names.

Now the only thing left to do is calling enable_attribute_scopes in models where you want to enable attribute scopes.


The above should give you an idea of how you could handle things, you could even add options like :except or :only. There is also the option to extract the code above into a module and extend AttributeScopeHelpers within ApplicationRecord if the class becomes to cluttered.

However, like I started this answer I would advise against adding scopes for each attribute.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52