当点/周期处于条件值时,ActiveRecord查询会更改

cor*_*ard 7 activerecord ruby-on-rails-3

请参阅底部的更新.我把它缩小了很多.

我还创建了一个讨论这个bug的准系统应用程序:https://github.com/coreyward/bug-demo

我还在官方跟踪器中创建了一张错误票:https://rails.lighthouseapp.com/projects/8994/tickets/6611-activerecord-query-changing-when-a-dotperiod-is-in-condition-值

如果有人可以告诉我如何修补它或解释Rails中发生的情况,我将非常感激.


我有一些奇怪/意外的行为.这让我相信有一个错误(确认这是一个错误将是一个完美的答案),或者我错过了一些正确的东西(或者我不明白).

代码

class Gallery < ActiveRecord::Base
  belongs_to :portfolio
  default_scope order(:ordinal)
end

class Portfolio < ActiveRecord::Base
  has_many :galleries
end

# later, in a controller action
scope = Portfolio.includes(:galleries) # eager load galleries
if some_condition
  @portfolio = scope.find_by_domain('domain.com')
else
  @portfolio = scope.find_by_vanity_url('vanity_url')
end
Run Code Online (Sandbox Code Playgroud)
  • 我有Portfolios哪些可以有多个Galleries.
  • galleriesordinal,vanity_urldomain属性.
  • gallery ordinals被设置为从零起整数.我已经确认这可以通过检查按预期工作Gallery.where(:portfolio_id => 1).map &:ordinal,它会[0,1,2,3,4,5,6]按预期返回.
  • 双方vanity_urldomaint.string, :null => false具有唯一索引的列.

问题

如果some_condition为true且find_by_domain已运行,则返回的库不符合默认范围.如果find_by_vanity_url运行,则根据默认范围对图库进行排序.我查看了生成的查询,它们非常不同.

查询

# find_by_domain SQL: (edited out additional selected columns for brevity)

Portfolio Load (2.5ms)  SELECT DISTINCT `portfolios`.id FROM `portfolios` LEFT OUTER JOIN `galleries` ON `galleries`.`portfolio_id` = `portfolios`.`id` WHERE `portfolios`.`domain` = 'lvh.me' LIMIT 1
Portfolio Load (0.4ms)  SELECT `portfolios`.`id` AS t0_r0, `portfolios`.`vanity_url` AS t0_r2, `portfolios`.`domain` AS t0_r11, `galleries`.`id` AS t1_r0, `galleries`.`portfolio_id` AS t1_r1, `galleries`.`ordinal` AS t1_r6 FROM `portfolios` LEFT OUTER JOIN `galleries` ON `galleries`.`portfolio_id` = `portfolios`.`id` WHERE `portfolios`.`domain` = 'lvh.me' AND `portfolios`.`id` IN (1)

# find_by_vanity_url SQL:

Portfolio Load (0.4ms)  SELECT `portfolios`.* FROM `portfolios` WHERE `portfolios`.`vanity_url` = 'cw' LIMIT 1
Gallery Load (0.3ms)  SELECT `galleries`.* FROM `galleries` WHERE (`galleries`.portfolio_id = 1) ORDER BY ordinal
Run Code Online (Sandbox Code Playgroud)

因此生成的查询find_by_domain没有ORDER语句,因此没有按需要排序.我的问题是......

为什么会这样?是什么促使Rails 3为这两列生成不同的查询?


更新

这真的很奇怪.我已经考虑并排除了以下所有内容:

  • 列上的索引
  • Rails中的保留/特殊单词
  • 表之间的列名冲突(即两个表上的域)
  • DB和Schema中的字段类型
  • "允许空"设置
  • 单独的范围

我得到find_by_vanity_url与位置,电话和标题相同的行为; 我得到find_by_domain与电子邮件相同的行为.


另一个更新

我把它缩小到参数在名称中有一个句点(.)的时候:

find_by_something('localhost') # works fine
find_by_something('name_routed_to_127_0_0_1') # works fine
find_by_something('my_computer.local') # fails
find_by_something('lvh.me') #fails
Run Code Online (Sandbox Code Playgroud)

我对内部人员不够熟悉,无法根据WHERE条件的值来确定查询形成的位置.

Geo*_*tte 8

这里的评论讨论了两种急切加载策略之间的区别

https://github.com/rails/rails/blob/3-0-stable/activerecord/lib/active_record/association_preload.rb

从文档:

# The second strategy is to use multiple database queries, one for each
# level of association. Since Rails 2.1, this is the default strategy. In
# situations where a table join is necessary (e.g. when the +:conditions+
# option references an association's column), it will fallback to the table
# join strategy.
Run Code Online (Sandbox Code Playgroud)

我相信"foo.bar"中的点会导致活动记录认为您正在将一个条件放在原始模型之外的表格上,该表格会提示文档中讨论的第二个策略.

两个单独的查询使用Person模型运行一个,第二个查询运行Item模型.

 Person.includes(:items).where(:name => 'fubar')

Person Load (0.2ms)  SELECT "people".* FROM "people" WHERE "people"."name" = 'fubar'
Item Load (0.4ms)  SELECT "items".* FROM "items" WHERE ("items".person_id = 1) ORDER BY items.ordinal
Run Code Online (Sandbox Code Playgroud)

因为您针对Item模型运行第二个查询,所以它继承了您指定的默认范围order(:ordinal).

第二个查询,它尝试使用完全运行人员模型进行加载,并且不会使用关联的默认范围.

 Person.includes(:items).where(:name => 'foo.bar')

Person Load (0.4ms)  SELECT "people"."id" AS t0_r0, "people"."name" AS t0_r1, 
"people"."created_at" AS t0_r2, "people"."updated_at" AS t0_r3, "items"."id" AS t1_r0, 
"items"."person_id" AS t1_r1, "items"."name" AS t1_r2, "items"."ordinal" AS t1_r3, 
"items"."created_at" AS t1_r4, "items"."updated_at" AS t1_r5 FROM "people" LEFT OUTER JOIN 
"items" ON "items"."person_id" = "people"."id" WHERE "people"."name" = 'foo.bar'
Run Code Online (Sandbox Code Playgroud)

这是一个小小的错误,但我可以看到它将如何呈现一个选项列表的几种不同的方式,以确保你捕获所有这些将是扫描完成的"WHERE"的方式点的条件并使用第二种策略,他们就这样离开,因为这两种策略都是有效的.我实际上会说,异常行为是在第一个查询中,而不是第二个查询中.如果您希望此查询的顺序仍然存在,我建议您执行以下操作之一:

1)如果您希望关联在调用时具有订单,则可以使用关联指定该关联.奇怪的是,这是在文档中,但我无法让它工作.

资料来源:http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many

class Person < ActiveRecord::Base
  has_many :items, :order => 'items.ordinal'
end
Run Code Online (Sandbox Code Playgroud)

2)另一种方法是将order语句添加到有问题的查询中.

Person.includes(:items).where(:name => 'foo.bar').order('items.ordinal')
Run Code Online (Sandbox Code Playgroud)

3)沿着相同的路线设置一个命名范围

class Person < ActiveRecord::Base
  has_many :items
  named_scope :with_items, includes(:items).order('items.ordinal')
end
Run Code Online (Sandbox Code Playgroud)

并称之为:

Person.with_items.where(:name => 'foo.bar')
Run Code Online (Sandbox Code Playgroud)


Bra*_*dan 5

这是Rails GitHub项目中的问题#950.看起来隐含的急切加载(导致此错误的原因)已在Rails 3.2中弃用并在Rails 4.0中删除.相反,您将明确告诉Rails您需要WHERE子句的JOIN - 例如:

Post.includes(:comments).where("comments.title = 'lol'").references(:comments)
Run Code Online (Sandbox Code Playgroud)

如果您迫切需要在Rails 3.1.*中修复此错误,那么您可能ActiveRecord::Relation#tables_in_string会在匹配表名时不那么激进.我创造了一个我的(不优雅和缓慢)解决方案的要点.这是差异:

diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 30f1824..d7335f3 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -528,7 +528,13 @@ module ActiveRecord
       return [] if string.blank?
       # always convert table names to downcase as in Oracle quoted table names are in uppercase
       # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
-      string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+      candidates = string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+      candidates.reject do |t|
+        s = string.partition(t).first
+        s.chop! if s.last =~ /['"]/
+        s.reverse!
+        s =~ /^\s*=/
+      end
     end
   end
 end
Run Code Online (Sandbox Code Playgroud)

它只适用于我的特定情况(Postgres和一个平等条件),但也许你可以改变它为你工作.