1

我正在嘗試將我的代碼從ActiveRecord 3升級到ActiveRecord 4,並且我相信我在ActiveRecord + SQLite3的布爾查詢支持中遇到了一個錯誤/迴歸。ActiveRecord 4中的迴歸+ 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

我已經調試了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語句:

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", ...]] 

注意,布爾列「啓用」的缺失,因爲在這種情況下,我們的真正/ 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 

,如果你玩弄'布爾值,而不是依賴於默認情況下,你可以糾正錯誤的好。

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版,我猜(不,我還沒有追查執行)的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不會救你。

我傾向於認爲,如果你給它像boolean_column: 1where('c in (?)', empty_array) ActiveRecord的應該拋出一個異常;告訴我,我是一個白癡會友好,幫助我悄悄地把事情弄得一團糟。

+0

感謝您的想法。我同時使用mysql和Sqlite3,因爲我使用內存中的SQLite3數據庫運行我的rspec測試,我相信這是在Ruby世界中的一個常見設置。因爲SQlite3是內存中的,所以沒有什麼可以「修復」。在嘗試調試這個問題之前,我以前從未交換過1/true。 – esilver

+0

這可能是常見的,但它仍然是一個壞主意(與Rails默認SQLite一樣糟糕,還是他們修復了第4版中的愚蠢行爲?),因爲數據庫可移植性是一個神話。 AR喜歡假裝它完全隱藏數據庫,但事實並非如此;考慮一些簡單的東西,比如在SQLite的一個'varchar(5)'中插入一個長度爲10的字符串(整個事情進去),MySQL(截斷爲5或者根據服務器配置得到一個異常)和PostgreSQL(異常來自數據庫)。不,驗證不會拯救你。 –