Imran Ahmad
Imran Ahmad

Reputation: 2927

Chain Scopes as OR Query

In model:

 scope :verified, -> { where(verified: true)}
 scope :active, -> { where(active: true) }

Now, Model.active.verified results as active and verified. How can I chain scopes as OR? Please note that I don't want to combine both scopes as one like:
where("active = ? OR verified = ?", true, true)

Upvotes: 0

Views: 979

Answers (1)

Nic Nilov
Nic Nilov

Reputation: 5155

You don't have to upgrade to Rails 5 in order to use or. There are ways of doing that in Rails 4. Although these do not lend themselves very well for being used in chained scopes, you can achieve similar level of convenience in building up your criterias.

  1. Arel tables. Arel is ActiveRecord's underlying implementation of relational algebra. It does support or among other powerful things.

    arel_table = Model.arel_table
    
    Model.where(
      arel_table[:active].eq(true).
      or(
        arel_table[:verified].eq(true)
      )
    )
    
    # => SELECT "models".* FROM "models" WHERE ("models"."active" = 't' OR "models"."verified" = 't')
    

As you can see, chaining criteria is complicated by the fact the or has to be applied inside where. Criteria could be built externally before passing to where but again, their hierarchical structure makes it less straightforward.

  1. Monkeypatching ActiveRecord, adding or implementation. Put this into config/initializers/active_record.rb:

    ActiveRecord::QueryMethods::WhereChain.class_eval do
      def or(*scopes)
        scopes_where_values = []
        scopes_bind_values  = []
        scopes.each do |scope|
          case scope
          when ActiveRecord::Relation
            scopes_where_values += scope.where_values
            scopes_bind_values += scope.bind_values
          when Hash
            temp_scope = @scope.model.where(scope)
            scopes_where_values += temp_scope.where_values
            scopes_bind_values  += temp_scope.bind_values
          end
        end
        scopes_where_values = scopes_where_values.inject(:or)
        @scope.where_values += [scopes_where_values]
        @scope.bind_values  += scopes_bind_values
        @scope
      end
    end
    

With this you will be able to do or queries like this:

Model.where.or(verified: true, active: true)

# => SELECT "models".* FROM "models" WHERE ("models"."verified" = $1 OR "models"."active" = $2)  [["verified", "t"], ["active", "t"]]

You can add more criteria like so:

Model.where.or(verified: true, active: true, other: false)

The query can be put in a class method like this:

def self.filtered(criteria)
  where.or(criteria)    # This is chainable!
end

or in scope, which is basically the same:

scope :filtered, lambda { |criteria| where.or(criteria) }

Since criteria is just a hash, you can build it in a convenient way with as many elements as you like:

criteria = {}
criteria[:verified] = true if include_verified?
criteria[:active] = true if include_active?
...

Model.filtered(criteria).where(... more criteria ...)

And there you have it. Read for more details these SO questions:

ActiveRecord Arel OR condition

OR operator in WHERE clause with Arel in Rails 4.2

Finally, in case you are not opposed to third-party solutions, take a look at the Squeel gem.

Upvotes: 1

Related Questions