ActiveRecord Arel OR 条件

Posted

技术标签:

【中文标题】ActiveRecord Arel OR 条件【英文标题】:ActiveRecord Arel OR condition 【发布时间】:2011-12-20 01:17:14 【问题描述】:

如何使用逻辑 OR 而不是 AND 组合 2 个不同的条件?

注意: 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'

但是如何生成以下具有 2 个不同的 Arel 关系可用的查询?

#=> select ... from ... where kind = 'admin' OR kind = 'author'

看来(according to Arel readme):

OR 运算符尚不支持

但我希望它不适用于这里并期望写出类似的东西:

(admins.or authors).to_sql

【问题讨论】:

可能会有所帮助:ActiveRecord OR query Hash notation 【参考方案1】:

ActiveRecord 查询是 ActiveRecord::Relation 对象(令人抓狂地不支持 or),而不是 Arel 对象(支持)。

[ 更新:从 Rails 5 开始,ActiveRecord::Relation 支持“或”;见https://***.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 来执行实际的数据库调用。

【讨论】:

这对我也有用。现在 Rails 4 发布了,这仍然是获得 OR 条件的最佳方法吗?【参考方案2】:

从 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')

【讨论】:

这可以很好地根据复杂表上的不同搜索获取多个 id,包括 Postgesql,其中“LIKE”不适用于整数。【参考方案3】:

我在寻找替代 mongoid 的 #any_of 的 activerecord 时遇到了同样的问题。

@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')))

编辑:从那时起,我发布了a gem to help building OR queries。

【讨论】:

【参考方案4】:

扩展jswanner answer(这实际上是一个很棒的解决方案并帮助了我)用于谷歌搜索的人:

你可以像这样应用范围

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))

【讨论】:

【参考方案5】:

这种方法怎么样:http://guides.rubyonrails.org/active_record_querying.html#hash-conditions(并检查 2.3.3)

admins_or_authors = User.where(:kind => [:admin, :author])

【讨论】:

请阅读问题。生成了 2 个无法控制的轨道关系。 没错,我应该更好地阅读这个问题。您特别声明:“但是,您如何生成以下已具有 2 个不同的 Arel 关系可用的查询?”,我不对此作出回应。事实上,当不支持“OR”条件范围时,您需要求助于黑客。所以道歉是有必要的。我只是想指出可能有一个更简单的解决方案可以帮助您从一开始就跳过问题。【参考方案6】:

我参加聚会有点晚了,但这是我能想到的最佳建议:

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'))"

【讨论】:

其实挺好的。谢谢。 这给了我一个“未定义的方法 .or for [string]”错误,但“#admins 或 #authors”效果很好。 bhaibel:我也遇到了同样的问题——它是由加入引起的。我改用连接模型,不再出现字符串错误。 如果您从真正的 ARel 查询开始,您可以跳过“where_values.reduce”步骤...查看我的答案 如果您的范围有多个条件,这将无法按预期工作(rails 3.2.12)。问题是括号没有放在 OR 条件周围,导致 AND 影响整个查询而不是部分【参考方案7】:

不幸的是它本身不支持,所以我们需要在这里破解。

而且 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。

【讨论】:

老实说,它可以工作,但是用这样的正则表达式编写 sql,我只需要对你的答案投反对票。那你还不如直接写sql,用find_by_sql,跳过整个Arel层。 那么请注意展示更好的解决方案。我提供了 2 个,其中一个我不喜欢也不使用。 三个人已经使用有效的 ARel 给出了有效的答案,但您已经接受了自己的丑陋答案,正如 Sjors 所说,您还不如一开始就操纵字符串来构造 SQL。投反对票。 老实说,如果您在 OR 的任何一侧有连接,Arel 拒绝编译查询,指的是“OR”操作数应该在结构上相同。所以在某些情况下,修改 SQL 是使这些东西正常工作的唯一方法,因为 Arel 仍然没有提供编译复杂 SQL 的良好动态方法。【参考方案8】:

使用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)

您可能认为我有偏见,但在这种特殊情况下,我仍然认为传统的数据结构操作比方法链接要清楚得多和方便。

【讨论】:

你能轻松地将范围转换为 SmartTuple 吗?我问是因为该应用程序已经大量使用了 Arel。 否,但您可以从代码中返回 SmartTuple 对象而不是返回范围,然后在需要时快速将 SmartTuple 转换为范围。【参考方案9】:

只需为您的 OR 条件创建一个范围:

scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])

【讨论】:

好吧,你的查询是不正确的 SQL :) 但你的建议正是我做不到的。请务必阅读问题。现在,您看到粗体字了吗? @DmytriiNagirniak 修复了 sql 错误,但我仍然不明白为什么这对你不起作用。也许它是 Arel 的东西(我仍然主要使用 2.3),或者这个问题需要更多的澄清。 这行得通。但正如我在问题中所说,我有 2 个范围。我需要使用 OR 语句组合那些。我无法将这 2 个作用域重写为单个 where 语句。【参考方案10】:

来自actual arel page:

OR 运算符的工作方式如下:

users.where(users[:name].eq('bob').or(users[:age].lt(25)))

【讨论】:

我看到了。尚不支持。它是如何回答问题的? 无法在评论中格式化。对 OR 运算符进行了测试,但您链接到的页面是 2009 年的,实际上并未使用 AREL。它可能回答问题,但至少它是正确的参考,并且没有说它不受支持。 好的。但是你不能用 Rails ActiveRecord 范围来做到这一点。您是否尝试过管理员和作者或类似的示例? ActiveRecord::Relation 上没有 or 方法。将其转换为 Arel 会产生另一组问题(查询是 SelectManager,而不是 Where)。还是我错过了什么? 哦,你以为你指的是 arel 因为你链接到它——对不起。 AFAIK,您可以将 arel 条件传递给 AR 方法,所以这应该可以工作:User.where(users[:name].eq('bob').or(users[:age].lt(25)))

以上是关于ActiveRecord Arel OR 条件的主要内容,如果未能解决你的问题,请参考以下文章

使用 ActiveRecord 3 / Arel 查找单个记录的最佳方法?

Rails:将复杂的 SQL 查询转换为 Arel 或 ActiveRecord

使用 Arel 进行嵌套集和连接查询并转换为 ActiveRecord::Relation

如何使用 Arel(大概)创建一个不影响 Rails 3 中的查询的 ActiveRecord 范围?

Rails ActiveRecord Arels:不支持的参数类型:字符串。改为构造一个 Arel 节点

Arel + Rails 4.2 导致问题(绑定丢失)