Объедините две именованные области с OR (вместо AND)
Я хочу найти все Annotations
чьи тела тоже:
- Равно "?"
- или же
- Подобно "[?]"
Какой лучший способ сделать это?
Я хотел бы использовать SearchLogic, если это возможно, но хотя SearchLogic позволяет вам выполнять каждое из следующих действий:
Annotation.body_equals('?')
Annotation.body_like('[?]')
и вы всегда можете связать их вместе: Annotation.body_equals('?').body_like('[?]')
Я не уверен, как их сочетать с OR
,
Обратите внимание, что вы можете комбинировать именованные области с OR
если их аргумент одинаков. Например, я мог бы сделать:
Annotation.body_equals_or_body_like('?')
Но это не поможет.
Обратите внимание, что я не привязан к SearchLogic, но это было бы замечательно для решения, которое не требует нарушения его абстракции.
5 ответов
Я не мог найти простых решений, но эта проблема заинтриговала меня, поэтому я нашел свое собственное решение:
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
Рассмотрим следующую настройку:
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
Вы называете это с именами ряда областей как таковые:
Person.or_scopes(:women, :children)
Это возвращает область как это:
Person.or_scopes(:women, :children).proxy_options
# => {:conditions=>"(`people`.`sex` = 'F') OR (age < 18)"}
Вы также можете вызвать его с массивом массивов, когда область требует параметров:
Person.or_scopes(:women, [:named, 'Sue']).proxy_options
# => {:conditions=>"(`people`.`sex` = 'F') OR (`people`.`name` = 'Sue')"}
В вашем случае Гораций, вы могли бы использовать следующее:
Annotation.or_scopes([:body_equals, '?'], [:body_like, '[?']).all
Я подошел к этому вопросу в поисках ответа на "или" двух named_scopes, и все ответы показались мне слишком сложными. Я немного исследовал и нашел решение, используя дополнительный named_scope, называемый "или", который делает свое дело.
Следуя приведенному примеру:
Annotation.body_equals('?')
Annotation.body_like('[?]')
оба возвращают объект named_scope, который создает выборки, возвращающие записи аннотаций
Теперь мы определяем другую именованную область видимости, ожидая две именованные области видимости как параметры:
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})})"
}}
Затем вы можете использовать:
Annotation.or(Annotation.body_equals('?'), Annotation.body_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 '%?%')
То, что вы были после
Так как named также является named_scope, можно связать его с другими named_scopes, включая другое или:
Annotation.or(Annotation.or(Annotation.body_equals('?'),
Annotation.body_like('[?]')),
Annotation.some_other)
Для Rails 2.x вы можете использовать следующую именованную область для имитации ИЛИ:
__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
Давайте использовать эту функцию, используя ваш пример выше.
q1 = Annotation.body_equals('?')
q2 = Annotation.body_like('[?]')
Annotation.or(q1,q2)
Приведенный выше код выполняет только один запрос. q1
а также q2
не храните результаты запроса, скорее, их класс ActiveRecord::NamedScope::Scope
,
or
named_scope объединяет эти запросы и объединяет условия с помощью ИЛИ.
Вы также можете вкладывать OR, как в этом надуманном примере:
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 ...>]
Так как or
возвращает ActiveRecord::NamedScope::Scope
Сам мы можем сойти с ума
# 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...>]
Я считаю, что большинство из этих примеров будет работать нормально, используя scope
s в Rails 3, хотя я не пробовал.
Немного бесстыдной саморекламы - эта функциональность доступна в геме fake_arel.
Разве результаты "Мне нравится" также не включают результаты "Равно"?
Вы также можете использовать именованную область в конце другого, чтобы создать действительно длинную именованную область. Из Searchlogic Docs (этот способ мне кажется немного надуманным):
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%'"
Или вы можете использовать объединение для объединения массивов результатов поиска при удалении дубликатов:
@equal_results = Annotation.body_equals('?')
@like_results = Annotation.body_like('[?]')
@results = @equal_results | @like_results
Вероятно, это
Annotation.body_equals_or_body_like(['?', '[?]'])