Combine two named scopes with OR (instead of AND)

前端 未结 5 1093
野的像风
野的像风 2020-12-03 08:56

I want to find all Annotations whose bodies are either:

  • Equal to \"?\"
  • or
  • Like \"[?]\"

What\'s t

相关标签:
5条回答
  • 2020-12-03 09:28

    Wouldn't the 'like' results also include the 'equals' results?

    You can also use a named scope on the end of another to make a really long named scope. From the Searchlogic Docs (this way seems a bit longwinded to me):

    User.username_or_first_name_like("ben")
    => "username LIKE '%ben%' OR first_name like'%ben%'"
    
    User.id_or_age_lt_or_username_or_first_name_begins_with(10)
    => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
    

    Or you can use a union to combine the search result arrays while removing the duplicates:

    @equal_results = Annotation.body_equals('?')
    @like_results  = Annotation.body_like('[?]')
    @results = @equal_results | @like_results
    
    0 讨论(0)
  • 2020-12-03 09:28

    I came over this question looking for the answer to "or" two named_scopes and all the answers looked too complex to me. I investigated a bit and found a solution using an additional named_scope called "or" which does the trick.

    Following the given example:

    Annotation.body_equals('?')
    Annotation.body_like('[?]')
    

    both return a named_scope object that construct selects returning annotation records

    now we define another named scope expecting two named scopes as parameters like:

    named_scope :or, lambda { |l, r| {
      :conditions => 
          "annotations.id IN (#{l.send(:construct_finder_sql,{:select => :id})}) or " + 
          "annotations.id IN (#{r.send(:construct_finder_sql,{:select => :id})})" 
    }}
    

    You can then use:

    Annotation.or(Annotation.body_equals('?'), Annotation.body_like('[?]'))
    

    This will create a query like:

    select * from annotations 
    where (annotations.id IN (select id from annotations where body='?') or
          (annotations.id IN (select id from annotations where body like '%?%')
    

    Which is what you were after

    As or is also a named_scope, it is possible to chain with other named_scopes including another or:

    Annotation.or(Annotation.or(Annotation.body_equals('?'), 
                                Annotation.body_like('[?]')),
                  Annotation.some_other)
    
    0 讨论(0)
  • 2020-12-03 09:28

    Probably it's

    Annotation.body_equals_or_body_like(['?', '[?]'])
    
    0 讨论(0)
  • 2020-12-03 09:30

    For Rails 2.x, you could use the following named scope to simulate OR:

        __or_fn = lambda do |*scopes|
        where = []
        joins = []
        includes = []
    
        # for some reason, flatten is actually executing the scope
        scopes = scopes[0] if scopes.size == 1
        scopes.each do |s|
          s = s.proxy_options
          begin
            where << merge_conditions(s[:conditions])
          rescue NoMethodError
            where << scopes[0].first.class.merge_conditions(s[:conditions])
          end
          joins << s[:joins] unless s[:joins].nil?
          includes << s[:include] unless s[:include].nil?
        end
        scoped = self
        scoped = scoped.includes(includes.uniq.flatten) unless includes.blank?
        scoped = scoped.joins(joins.uniq.flatten) unless joins.blank?
        scoped.where(where.join(" OR "))
      end
      named_scope :or, __or_fn
    

    Let's use this function using your example above.

    q1 = Annotation.body_equals('?')
    q2 = Annotation.body_like('[?]')
    Annotation.or(q1,q2)
    

    The above code executes only one query. q1 and q2 do not hold the results of the query, rather, their class is ActiveRecord::NamedScope::Scope.

    The or named_scope combines these queries and joins the conditions with an OR.

    You could also nest ORs, like in this contrived example:

    rabbits = Animal.rabbits
    #<Animal id: 1 ...>
    puppies = Animal.puppies
    #<Animal id: 2 ...>
    snakes = Animal.snakes
    #<Animal id: 3 ...>
    lizards = Animal.lizards
    #<Animal id: 4 ...>
    
    Animal.or(rabbits, puppies)
    [#<Animal id: 1 ...>, #<Animal id: 2 ...>]
    Animal.or(rabbits, puppies, snakes)
    [#<Animal id: 1 ...>, #<Animal id: 2 ...>, #<Animal id: 3 ...>]
    

    Because or returns a ActiveRecord::NamedScope::Scope itself, we can go really crazy:

    # now let's get crazy
    or1 = Animal.or(rabbits, puppies)
    or2 = Animal.or(snakes, lizards)
    Animal.or(or1, or2)
    [#<Animal id: 1 ...>, #<Animal id: 2 ...>, #<Animal id: 3 ...>, #<Animal id: 4...>]
    

    I believe that most of these examples would work fine using scopes in Rails 3, although I have not tried.

    A Bit of shameless self-promotion - This functionality is available in the fake_arel gem.

    0 讨论(0)
  • 2020-12-03 09:45

    I couldn't find any simple solutions, but this problem intrigued me, so I rolled my own solution:

    class ActiveRecord::Base
    
      def self.or_scopes(*scopes)
        # Cleanup input
        scopes.map! do |scope|
          scope = scope.respond_to?(:to_a) ? scope.to_a : [*scope]
          scope.unshift(scope.shift.to_sym)
        end
    
        # Check for existence of scopes
        scopes.each{|scope| raise ArgumentError, "invalid scope: #{scope.first}" unless self.scopes.has_key?(scope.first) }
    
        conditions = scopes.map do |scope|
          scope = self.scopes[scope.first].call(self, *scope[1..-1])
          self.merge_conditions(scope.proxy_options[:conditions])
        end
    
        or_conditions = conditions.compact.join(" OR ")
    
        merged_scopes = scopes.inject(self){|merged, scope| merged.scopes[scope.first].call(self, *scope[1..-1]) }
    
        # We ignore other scope types but so does named_scopes
        find_options = merged_scopes.scope(:find).merge(:conditions => or_conditions)
    
        self.scoped(find_options)
      end
    
    end
    

    Consider the following setup:

    class Person < ActiveRecord::Base
      named_scope :men,      :conditions => { :sex => 'M' }
      named_scope :women,    :conditions => { :sex => 'F' }
      named_scope :children, :conditions => "age < 18"
      named_scope :named, lambda{|name|
        { :conditions => { :name => name } }
      }
    end
    

    You call it with the names of a series of scopes as such:

    Person.or_scopes(:women, :children)
    

    This returns a scope like this:

    Person.or_scopes(:women, :children).proxy_options
    # => {:conditions=>"(`people`.`sex` = 'F') OR (age < 18)"}
    

    You can also call it with an array of arrays when the scope requires parameters:

    Person.or_scopes(:women, [:named, 'Sue']).proxy_options
    # => {:conditions=>"(`people`.`sex` = 'F') OR (`people`.`name` = 'Sue')"}
    

    In your case Horace, you could use the following:

    Annotation.or_scopes([:body_equals, '?'], [:body_like, '[?']).all
    
    0 讨论(0)
提交回复
热议问题