【问题标题】:Regression in ActiveRecord 4 + SQLite3 boolean query support?ActiveRecord 4 + SQLite3 布尔查询支持中的回归?
【发布时间】:2014-01-30 17:26:07
【问题描述】:

我正在尝试将我的代码从 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 发生了变化?

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

【问题讨论】:

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


    【解决方案1】:

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

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

    ActiveRecord 在与 SQLite 交互时将布尔值映射到字符串类型“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 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 INTO "store_items" ("created_at", "other_columns....") VALUES (?, ?, ?)  [["created_at", 2014-01-11 21:47:24 UTC], ["other_colummns", ...]]
    

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

    由于 ActiveRecord 生成的插入语句未指定启用,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
    

    或者,如果您“调整”布尔值而不是依赖默认值,您也可以更正错误。

    【讨论】:

      【解决方案2】:

      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 完全不同,而在 PostgreSQL 中,无论您使用的是哪个 ActiveRecord 版本,它们都是不同的。

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

      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 不会拯救你。

      我倾向于认为如果你给 ActiveRecord 提供boolean_column: 1where('c in (?)', empty_array) 之类的东西,它应该抛出异常;告诉我我是个白痴会友好,但帮助我悄悄地把事情搞砸不是。

      【讨论】:

      • 感谢您的想法。我同时使用 mysql 和 Sqlite3,因为我使用内存中的 SQLite3 数据库运行我的 rspec 测试,我相信这是 ruby​​ 世界中的常见设置。因为 SQlite3 是在内存中的,所以没有什么可以“修复”的。在尝试调试此问题之前,我从未交替使用 1/true。
      • 这可能很常见,但它仍然是一个坏主意(就像 Rails 默认使用 SQLite 一样糟糕,还是他们在 v4 中修复了这种愚蠢?)因为数据库可移植性是一个神话。 AR 喜欢假装它完全隐藏了数据库,但事实并非如此;考虑一些简单的事情,例如在 SQLite 中的 varchar(5) 中插入长度为 10 的字符串(整个过程都进入)、MySQL(截断为 5 或根据服务器配置获取异常)和 PostgreSQL(来自数据库的异常)之间的区别)。不,验证不会拯救你。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-25
      • 2013-03-22
      • 1970-01-01
      • 1970-01-01
      • 2014-06-07
      • 2011-11-19
      相关资源
      最近更新 更多