ActiveRecord 4中的回归+ SQLite3布尔查询支持?

时间:2014-01-11 18:40:54

标签: mysql sqlite activerecord ruby-on-rails-4 rails-activerecord

我正在尝试将我的代码从ActiveRecord 3升级到ActiveRecord 4,我相信我在ActiveRecord + SQLite3的布尔查询支持中遇到了错误/回归。

以下是运行ActiveRecord 4.0.2的IRB会话的输出,其中SQLite3是数据库后端:

2.0.0p353 :040 > StoreItem.where(item_class: 1, enabled: 1).order(item_order: :desc).count
 => 4 
2.0.0p353 :041 > StoreItem.where(item_class: 1, enabled: true).order(item_order: :desc).count
 => 0 

作为比较点,当Mysql 5.5是数据库后端时,这是相同的输出:

2.0.0p353 :005 > StoreItem.where(item_class: 1, enabled: 1).order(item_order: :desc).count
 => 4 
2.0.0p353 :006 > StoreItem.where(item_class: 1, enabled: true).order(item_order: :desc).count
 => 4 

现在,让我们看看使用AR 3.2.14运行时会发生什么:

SQLite3的:

2.0.0p353 :005 > StoreItem.where(item_class: 1, enabled: 1).order(item_order: :desc).count
 => 0 
2.0.0p353 :006 > StoreItem.where(item_class: 1, enabled: true).order(item_order: :desc).count
 => 4 

Mysql 5.5:

2.0.0p353 :001 > StoreItem.where(item_class: 1, enabled: 1).order(item_order: :desc).count
 => 4 
2.0.0p353 :002 > StoreItem.where(item_class: 1, enabled: true).order(item_order: :desc).count
 => 4 

正如您所看到的,当出现布尔查询时,ActiveRecord 3.2.14和4.0.2在SQLite3中完全相反。

我刚检查了实际生成的SQL,它是完全相同的。第一个查询如下所示:

SELECT COUNT(*) FROM "store_items" WHERE "store_items"."item_class" = 1 AND "store_items"."enabled" = 1

第二个看起来像这样:

SELECT COUNT(*) FROM "store_items" WHERE "store_items"."item_class" = 1 AND "store_items"."enabled" = 't'

因此,在处理布尔列值时,SQLite3可能从1.3.5变为1.3.8?

这是一个已知的错误,任何人都可以评论原因吗?

2 个答案:

答案 0 :(得分:2)

我调试了ActiveRecord 3.2.14和4.0.2的内容。这是从头到尾的错误:

SQLite允许您插入任意字符串/数字作为布尔列的列值。因此,您可以为布尔列值插入1或't'。

当与SQLite交互时,ActiveRecord将布尔值映射到字符串类型't'或'f',而不是1或0.这个决定背后的原因可能来自Postgres,但现在就是这样。

当ActiveRecord第一次在3.2.14 at line 365 in persistence.rb中创建记录时,它会创建所有模型字段的映射并插入所有字段,无论它们是否已被更改。这是创建方法:

def create
  attributes_values = arel_attributes_values(!id.nil?)

  new_id = self.class.unscoped.insert attributes_values

  self.id ||= new_id if self.class.primary_key

  IdentityMap.add(self) if IdentityMap.enabled?
  @new_record = false
  id
end

这会导致ActiveRecord生成如下所示的insert语句(注意是否存在enabled,我们的布尔列):

INSERT INTO "store_items" ("created_at", "enabled", "other_columns....") VALUES (?, ?, ?)  [["created_at", 2014-01-11 21:47:24 UTC], ["enabled", true], ["other_colummns", ...]]

在ActiveRecord 4.0.2中,文件dirty.rb, line 78现在调用persistence.rb (at line 507)的create_record方法。创建记录如下所示:

def create_record(attribute_names = @attributes.keys)
  attributes_values = arel_attributes_with_values_for_create(attribute_names)

  new_id = self.class.unscoped.insert attributes_values
  self.id ||= new_id if self.class.primary_key

  @new_record = false
  id
end

因为create_record现在接受一个只列出已更改的列的参数,所以它会生成一个insert语句,该语句不包含默认值与您插入的列匹配的列。因此,使用与默认值匹配的布尔值的insert语句将如下所示:

INSERT INTO "store_items" ("created_at", "other_columns....") VALUES (?, ?, ?)  [["created_at", 2014-01-11 21:47:24 UTC], ["other_colummns", ...]]

请注意,缺少布尔列“enabled”,因为在这种情况下,我们的默认值true / 1与我们第一次创建记录时插入的内容相匹配。

因为ActiveRecord生成的insert语句没有指定enabled,所以SQLite给它的值为1,而不是't'的值,这是ActiveRecord 3.1.14用来给它的。

最终,要解决此错误,请不要在布尔列上包含默认值,或者确保将其更改为非默认值,以强制ActiveRecord将其实际设置为“t”或“f” '创造价值。

因此,改变这个:

class CreateStoreItems < ActiveRecord::Migration
  def change
    create_table :store_items do |t|
      t.boolean :enabled, :null => false, :default => 1
    end
  end
end

到这个

class CreateStoreItems < ActiveRecord::Migration
  def change
    create_table :store_items do |t|
      t.boolean :enabled, :null => false
    end
  end
end

或者,如果您'旋转'布尔值而不是依赖默认值,您也可以更正错误。

答案 1 :(得分:0)

ActiveRecord与SQLite的布尔值有混淆的历史。你可以在这里阅读Rails3中的一些问题:

  

Rails 3 SQLite3 Boolean false

执行摘要是:

  • SQLite本身使用C风格的零和一个布尔值而不是真正的布尔类型。
  • MySQL本身使用C风格的零和一个布尔值而不是真正的布尔类型。
  • SQLite驱动程序(至少由Rails3决定)错误地使用't''f'字符串作为布尔文字。这些实际上是PostgreSQL的本地boolean类型的字符串表示,SQLite驱动程序似乎意外地从基本驱动程序继承它们,这可能是为PostgreSQL编写的。

这意味着,假设enabled是一个布尔列,enabled: 1enabled: true在MySQL中应该是相同的,但这是代码中的一个错误,它会意外地假设有关底层数据库的内容行为。在Rails3中,enabled: 1enabled: true与SQLite完全不同,无论您使用哪种ActiveRecord版本,它们在PostgreSQL中都是不同的。

在ActiveRecord的Rails4版本中,我猜测(不,我没有追踪执行),有时会在quote with this kludgery处理SQLite布尔混淆:

case value
  #...
  when true, false
    if column && column.type == :integer
      value ? '1' : '0'
    else
      value ? quoted_true : quoted_false
    end

所以如果你发送一个真正的Ruby布尔值,它应该被克服到数据库的正确值,但如果你发送一个整数,所有的赌注都是关闭的。类似的废话出现在type_cast

我认为你还有一些工作要做:

  1. 不要假设1true是相同的,0false也是如此。如果要查询数据库中的布尔值,请使用truefalse
  2. 修复SQLite数据库,以便所有布尔列使用't''f'来匹配SQLite驱动程序的混淆。您还需要检查MySQL中所有布尔列的值,以确保,MySQL还可以快速执行规则,因此它可以在您的背后做出奇怪的事情。 MySQL并不像SQLite那么快,但它们都会以友好性的名义弯曲规则。
  3. 停止使用两个不同的数据库系统,除非您准备手动强制实施可移植性。数据库可移植性在很大程度上是一个神话,除非你非常小心,只能在婴儿谈话SQL中与数据库对话,或者你编写自己的可移植层并写入该API而不是ORM的本机API。不,AR不会救你。
  4. 我倾向于认为如果你给它boolean_column: 1where('c in (?)', empty_array)这样的话,ActiveRecord应该抛出异常;告诉我,我是一个白痴会友好,帮助我悄悄弄乱一些东西不是。