如何使用逻辑OR而不是AND来组合两个不同的条件?
注意: 2条件是作为rails作用域生成的,不能轻易地直接更改为where("x or y")
。
简单的例子:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
很容易应用AND条件(在这种特殊情况下是没有意义的):
(admins.merge authors).to_sql
#=> select ... from ... where kind = 'admin' AND kind = 'author'
但是你怎么能产生下面的查询有两个不同的Arel关系已经可用?
#=> select ... from ... where kind = 'admin' OR kind = 'author'
看起来(根据Arel自述):
不支持OR操作符
但我希望它不适用于这里,并期望写这样的:
(admins.or authors).to_sql
ActiveRecord查询是ActiveRecord::Relation
对象(令人恼火的是不支持or
),而不是Arel对象(这样做)。
[UPDATE:从Rails 5开始,ActiveRecord::Relation
支持"or";参见https://stackoverflow.com/a/33248299/190135]
但幸运的是,他们的where
方法接受ARel查询对象。所以如果User < ActiveRecord::Base
…
users = User.arel_table
query = User.where(users[:kind].eq('admin').or(users[:kind].eq('author')))
query.to_sql
现在显示放心:
SELECT "users".* FROM "users" WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))
为了清晰起见,您可以提取一些临时的部分查询变量:
users = User.arel_table
admin = users[:kind].eq('admin')
author = users[:kind].eq('author')
query = User.where(admin.or(author))
当然,一旦您有了查询,您可以使用query.all
来执行实际的数据库调用。
我来晚了一点,但这是我能想到的最好的建议:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)
User.where(admins.or(authors)).to_sql
# => "SELECT "users".* FROM "users" WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))"
从Rails 5开始,我们有了ActiveRecord::Relation#or
,允许你这样做:
User.where(kind: :author).or(User.where(kind: :admin))
…它被转换成您期望的SQL:
>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = 'author' OR "users"."kind" = 'admin')
从实际的arel页面:
OR操作符是这样工作的:
users.where(users[:name].eq('bob').or(users[:age].lt(25)))
我遇到了同样的问题,寻找一个activerecord替代mongoid的#any_of
。
@jswanner答案很好,但只有当where参数是Hash时才有效:
> User.where( email: 'foo', first_name: 'bar' ).where_values.reduce( :and ).method( :or )
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>
> User.where( "email = 'foo' and first_name = 'bar'" ).where_values.reduce( :and ).method( :or )
NameError: undefined method `or' for class `String'
要同时使用字符串和散列,可以这样:
q1 = User.where( "email = 'foo'" )
q2 = User.where( email: 'bar' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )
确实,这很难看,你不想每天都使用它。以下是我所做的一些#any_of
实现:https://gist.github.com/oelmekki/5396826
它允许这样做:
> q1 = User.where( email: 'foo1' ); true
=> true
> q2 = User.where( "email = 'bar1'" ); true
=> true
> User.any_of( q1, q2, { email: 'foo2' }, "email = 'bar2'" )
User Load (1.2ms) SELECT "users".* FROM "users" WHERE (((("users"."email" = 'foo1' OR (email = 'bar1')) OR "users"."email" = 'foo2') OR (email = 'bar2')))
编辑:从那时起,我已经发布了一个gem来帮助构建OR查询。
为OR条件创建一个作用域:
scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])
使用SmartTuple,它将看起来像这样:
tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)
或
User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)
你可能认为我有偏见,但我仍然认为在这种特殊情况下,传统的数据结构操作远比方法链更清晰和方便。
扩展jswanner的答案(这实际上是一个很棒的解决方案,帮助了我):
你可以像这样应用作用域
scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
with_glob = where(owner_id: nil).where_values.reduce(:and)
where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}
User.with_owner_ids_or_global(Developer, 1, 2)
# => ...WHERE `users`.`owner_type` = 'Developer' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
这个方法怎么样:http://guides.rubyonrails.org/active_record_querying.html#hash-conditions(并检查2.3.3)
admins_or_authors = User.where(:kind => [:admin, :author])
不幸的是,它不支持本机,所以我们需要在这里进行hack。
和hack看起来像这样,这是相当低效的SQL(希望dba没有看到它:-)):
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
both = User.where("users.id in (#{admins.select(:id)}) OR users.id in (#{authors.select(:id)})")
both.to_sql # => where users.id in (select id from...) OR users.id in (select id from)
生成子选择集。
一个更好的hack(从SQL的角度)看起来像这样:
admins_sql = admins.arel.where_sql.sub(/^WHERE/i,'')
authors_sql = authors.arel.where_sql.sub(/^WHERE/i,'')
both = User.where("(#{admins_sql}) OR (#{authors_sql})")
both.to_sql # => where <admins where conditions> OR <authors where conditions>
这会生成适当的OR条件,但显然它只考虑了作用域的WHERE部分。
我选择了第一个,直到我看到它的性能如何。
在任何情况下,您都必须非常小心,并观察生成的SQL