为什么Rails在使用范围查询的where子句哈希语法时会添加`OR 1=0`?

19
我正在从事的项目在RDS上使用MySQL(具体来说是mysql2 gem)。 当我在 where 语句中使用包含范围的条件哈希时,我的查询会出现一些奇怪的附加内容。
User.where(id: [1..5])

User.where(id: [1...5])

分别执行以下查询,得到的结果如下:

SELECT `users`.* FROM `users` WHERE ((`users`.`id` BETWEEN 1 AND 5 OR 1=0))
SELECT `users`.* FROM `users` WHERE ((`users`.`id` >= 1 AND `users`.`id` < 5 OR 1=0))

OR FALSE 是一个无效操作,因此查询工作得非常好。我只是想知道为什么Rails或ARel要将这个片段添加到查询中。

编辑

看起来能够解释这个问题的代码在 ActiveRecord::PredicateBuilder 的第26行。但是还不清楚为什么此时哈希表会是空的?也许其他人知道。

编辑2

这很有趣。我正在查看 Filip 的评论,以了解他为什么要这样做,因为它似乎只是一个澄清,但他正确地指出,1..5 != [1..5]。前者是从1到5的包含范围,而后者是其第一个元素就是前者的数组。我尝试将它们放入 ARel 的 where 调用中,以查看生成的 SQL,但没有出现 OR 1=0

User.where(id: 1..5) #=> SELECT "users".* FROM "users"  WHERE ("users"."id" BETWEEN 1 AND 5)
User.where(id: 1...5) #=> SELECT "users".* FROM "users"  WHERE ("users"."id" >= 1 AND "users"."id" < 5)

虽然我仍然不知道为什么ARel会添加OR 1=0,它将始终为假且似乎是不必要的。这可能是由于处理ArrayRange的方式不同。


3
[1..5] 不是范围。[1..5].class #> Arraya = [0,1,2,3,4,5,6]; a[1..4] #> [1,2,3,4] @aaron - Filip Bartuzi
5个回答

12

基于你已经发现的事实,即[1..5]不是指定范围的正确方式...我已经发现了为什么[1..5]的行为表现如此。为了到达那里,我首先发现在哈希条件中的空数组会产生1=0 SQL条件:

User.where(id: []).to_sql
# => "SELECT \"users\".* FROM \"users\"  WHERE 1=0"

而且,如果你检查 ActiveRecord::PredicateBuilder::ArrayHandler 代码,你会发现数组值总是被分成区间和其他值。

ranges, values = values.partition { |v| v.is_a?(Range) }
这就解释了为什么在使用非范围值时看不到1=0。也就是说,如果要从数组中获取1=0而没有包含范围,唯一的方法是提供一个空数组,这会产生上面显示的1=0条件。当数组中只有一个范围时,你将获得范围条件(ranges)和一个单独的空数组条件(values)执行。我猜这并没有一个很好的理由...它只是更容易让它保持原样而不是避免它(因为结果集是等效的)。如果分区代码聪明一些,它就不必添加额外的、空的values数组,并且可以跳过1=0条件。
至于最初的1=0来自哪里...我认为它来自数据库适配器,但我找不到确切的位置。然而,我认为这是一个失败查找记录的尝试。换句话说,WHERE 1=0永远不会返回任何用户,这比其他SQL语句更合理,例如WHERE id=null,它会找到任何id为空的用户(意识到这并不是真正正确的SQL语法)。当我们尝试查找所有ID在空集中的用户时,这就是我所期望的(也就是说,我们不要求nil ID或null ID等)。因此,在我的脑海中,把关于1=0的确切位置留作一个黑盒子是可以接受的。至少现在我们可以思考为什么数组中的范围会导致它出现!

更新

我还发现,即使直接使用ARel,你仍然可以得到1=0:
User.arel_table[:id].in([]).to_sql
# => "1=0"

谢谢!这真的解释了奇怪的行为。 - Aaron

1
这只是一个猜测,因为我在自己的项目中做了类似的事情(尽管我使用了AND 1)。
无论出于什么原因,在生成查询时,始终包含一个无操作的WHERE子句要比有条件地生成WHERE子句更容易。也就是说,如果不包含任何where部分,它仍将生成一些有效的内容。
另一方面,我不确定为什么它采用这种形式:当我使用1 [<AND (generated code)>...]时,它允许任意链接,但我不知道您所看到的会如何允许它。尽管如此,我仍然认为这可能是算法代码生成方案的结果。

猜测总比没有好,但请帮我理解其背后的逻辑。如果我不使用ActiveRecord::where方法,那么我根本不会生成where子句,包括no-op。你在Rails中使用过那个1 AND ...查询吗?我不确定为什么需要它以及它如何促进链接。 - Aaron
有趣。另外,http://stackoverflow.com/questions/19897107/rails-adds-and-1-0-to-queries - zebediah49
我找到了那个,但我没有使用 CanCan :/。 - Aaron

0

检查一下你是否正在使用active_record-acts_as。这是我的问题所在。

将以下行添加到您的Gemfile中:

gem 'active_record-acts_as', :git => 'https://github.com/hzamani/active_record-acts_as.git'

这只会拉取最新版本的 Gem,希望已经修复了。对我有用。


我没有使用这个 gem,所以它不可能是奇怪 SQL 的源头。我只是根据评论添加了额外的信息。也许这会帮助某人回答这个问题。 - Aaron

0

我认为你个人正在看到 Ruby 的副作用。

我认为更好的做法是使用:

2.0.0-p481@meri :008 > [*1..5]
 => [1, 2, 3, 4, 5]

User.where(id: [*1..5]).to_sql
"SELECT `users`.* FROM `users`  WHERE `users`.`id` IN (1, 2, 3, 4, 5)"

这会创建一个数组,而不是一个类为 Range 的元素 1 的数组。

或者

使用显式的范围来触发 AREL 中的 BETWEEN。

# with end element, i.e. exclude_end=false
2.0.0-p481@meri :013 > User.where(id: Range.new(1,5)).to_sql
=> "SELECT `users`.* FROM `users`  WHERE (`users`.`id` BETWEEN 1 AND 5)"

# without end element, i.e. exclude_end=true
2.0.0-p481@meri :022 > User.where(id: Range.new(1, 5, true)).to_sql
 => "SELECT `users`.* FROM `users`  WHERE (`users`.`id` >= 1 AND `users`.`id` < 5)"

这些也是指定ARel条件范围的好方法。@pdobb解释了为什么出现了OR 1=0 - Aaron

-1

如果你关心对所生成的查询有控制权以及 SQL 语言和数据库功能的全部能力,那么我建议从 ActiveRecord/Arel 转移到 Sequel。

我可以诚实地说,在使用 ActiveRecord 时,特别是当你超越简单的 CRUD 查询时,你将会遇到更多的怪癖和令人恼火的时刻。当你开始尝试愤怒地查询数据时,也许需要在这里和那里连接几个连接表,并意识到你确实需要连接条件或 union all 类型的查询。

它的查询生成和结果处理速度也显著更快、更可靠,而且更容易组合你想要的查询。它还有真正的文档,你可以真正阅读,不像 arel。

我只希望我早些时候就发现了它,而不是坚持使用 Rails 默认的数据访问层。


这个库 https://github.com/jeremyevans/sequel ?我之前没听说过,但我会去看看。 - Aaron
是的,就是那个。还有 sequel-rails 集成 gem。如果你正在使用 postgres(我也强烈推荐),那么还需要 sequel-pg gem,它提供更快的结果处理和流式传输。像任何东西一样,它有一个学习曲线,但你也可以做更多的事情。 - Andrew Hacking

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接