2

我在這裏使用Rails 2.3.14,但它可能存在的問題並不侷限於此特定版本。這是Rails 3中仍然存在的所有關聯和熱切加載功能,並且在2.3.14之前已經出現。我正在從rails 2.3.8升級的過程中,我沒有遇到下面描述的問題。Rails與多態關聯的STI模型,渴望加載查詢引用錯誤的類類型

下面的代碼是一個基於更復雜的生產系統的模型。我列出的類/模塊方案是由於某種原因而設置的。實際上,我在這個模型中包含了一些比展示問題所需的更多細節,希望能夠使系統的整體結構更加清晰。

假設我有幾個可以「驅動」的域對象,包括車輛(汽車/卡車)和高爾夫球。對於這些事情,我有一個ActiveRecord類:

class Vehicle < ActiveRecord::Base 
end 

class Car < Vehicle 
    include Driveable 
end 

class Truck < Vehicle 
    include Driveable 
end 

class GolfBall < ActiveRecord::Base 
    include Driveable 
end 

首先要注意GolfBall是一個頂級模型類和相應的數據庫表golf_balls。另一方面,CarTruckVehicle的子類別。車輛數據庫設置爲STI,所以CarsTrucks都對應於vehicles表(具有type列差異)。

其次,請注意,我包括在所有最下層的領域對象(CarTruckGolfBall)一個Drivable模塊,它看起來像這樣(在實際系統中該模塊可以做更多的也一樣,包括設置的東西了基於特定的包括域對象):

module Driven 
    def self.included(base) 
     base.class_eval do 
      has_one :driver, :as => :driveable, :class_name => "#{self.name}Driver", :dependent => :destroy 
     end 
    end 
end 

因此每這些事情可以有一個Driver,而且它使用基於包括類名:class_name(例如,包括類Car導致has_one:class_name => "CarDriver"),因爲每個這些引用類(CarDriver等)包含該協會使用所需的特定業務邏輯。

有一個頂層Driver類建立多態關聯,然後類似的子類層次結構如以上針對域對象驅動程序:

class Driver < ActiveRecord::Base 
    belongs_to :driveable, :polymorphic => true 
end 

class VehicleDriver < Driver 
end 

class CarDriver < VehicleDriver 
end 

class TruckDriver < VehicleDriver 
end 

class GolfBallDriver < Driver 
end 

這是基於單個數據庫表drivers,使用STI適用於所有亞類。

有了這個系統的地方,我創建了一個新的Car(存儲在下面@car),並將其與新創建CarDriver這樣的(它分裂成在這個模擬了這些特殊的順序步驟鏡像的方式聯繫起來實際系統的工作原理):

@car = Car.create 
CarDriver.create(:driveable => @car) 

此創建的數據庫行中的vehicles表是這樣的:

id type ... 
----------------- 
1 Car ... 

而且像drivers表中的行這樣的:

id driveable_id driveable_type type   ... 
-------------------------------------------------------- 
1 1    Vehicle   CarDriver ... 

Vehicledriveable_type,而不是Car因爲車輛是STI。到現在爲止還挺好。現在,我開了一個軌道控制檯,並執行一個簡單的命令來獲取Car實例:

>> @car = Car.find(:last) 
=> #<Car id: 1, type: "Car", ...> 

根據日誌,這裏是已執行的查詢:

Car Load (1.0ms) 
SELECT * FROM `vehicles` 
WHERE (`vehicles`.`type` = 'Car') 
ORDER BY vehicles.id DESC 
LIMIT 1 

然後我得到的CarDriver

>> @car.driver 
=> #<CarDriver id: 1, driveable_id: 1, driveable_type: "Vehicle", type: "CarDriver", ...> 

這導致此查詢被執行。

CarDriver Load (0.7ms) 
SELECT * FROM `drivers` 
WHERE (`drivers`.driveable_id = 1 AND `drivers`.driveable_type = 'Vehicle') AND (`drivers`.`type` = 'CarDriver') 
LIMIT 1 

但是,如果我嘗試使用急切加載,我會得到不同的結果。從新的控制檯會話中,我運行:

>> @car = Car.find(:last, :include => :driveable) 
=> #<Car id: 1, type: "Car", ...> 
>> @car.driver 
=> nil 

這導致驅動程序的nil。檢查日誌,第一條語句執行以下查詢(定期查詢和預先加載查詢):

Car Load (1.0ms) 
SELECT * FROM `vehicles` 
WHERE (`vehicles`.`type` = 'Car') 
ORDER BY vehicles.id DESC 
LIMIT 1 

CarDriver Load (0.8ms) 
SELECT * FROM `drivers` 
WHERE (`drivers`.driveable_id = 1 AND `drivers`.driveable_type = 'Car') AND (`drivers`.`type` = 'CarDriver') 
LIMIT 1 

正如你所看到的,在預先加載的情況下,Car查詢是與上述相同,但CarDriver查詢是不同的。它幾乎是一樣的,除了drivers.driveable類型,它正在尋找Car它應該在那裏尋找STI基類名稱,Vehicle,因爲它在非渴望加載的情況下。

任何想法如何解決這個問題?

回答

2

在閱讀Rails源代碼幾個小時後,我很確定我明白了這一點(當然,這可能是我錯誤閱讀的東西)。它似乎是Rails 2.3.9中引入的Rails 2.3.x分支中的一個bug,從未修復。它開始這個Rails的2.3 bug報告:

Query built wrong for preloading associations nested under polymorphic belongs_to

事實上,這是一個有效的BUG,並且它固定這個犯的Rails 2.3.9:

Fix eager loading of polymorphic has_one associations nested

然而,這個提交無意中破壞了STI類中的非嵌套多態熱切加載,並且他們沒有針對這種情況進行測試,所以自動化測試沒有抓住它。相關的差異是在這裏:

360 -   conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" 

360 +   parent_type = if reflection.active_record.abstract_class? 
361 +   self.base_class.sti_name 
362 +   else 
363 +   reflection.active_record.sti_name 
364 +   end 
365 + 
366 +   conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{parent_type}'" 

從這個差異,你可以看到,之前2.3.9爲渴望加載多態關聯它總是使用self.base_class.sti_nameparent_type(這是多態類型列)。按照問題中的示例,這意味着父類型列將是driveable_type,並且因爲Car.base_classVehicle,它將在driveable_type = 'Vehicle'上正確匹配。

但是,從上述提交開始,只有關聯持有者是abstract_class才能正確執行此操作。這似乎是一個直接的錯誤;如果協會成員是抽象的和sti子類,它應該被設置,所以它應該將parent_type設置爲爲self.base_class.sti_name

幸運的是,有一種簡單的方法可以解決這個問題。雖然它沒有在任何地方的文檔中提到過,但似乎可以通過在持有多態belongs_to關聯的sti子類上設置abstract_class = true來解決此問題。在這個問題的例子,這將是:

class Car < Vehicle 
    abstract_class = true 
    include Driveable 
end 

class Truck < Vehicle 
    abstract_class = true 
    include Driveable 
end 

做一個全文檢索的軌道源abstract_class的,設置,似乎並不像它會有什麼意想不到/不良的後果,並在與真正的應用程序的初始測試,它似乎已經解決了這個問題。

1

對不起,發佈此問題作爲答案,但由於某種原因,您需要有足夠的信譽來添加評論,並且沒有其他方式可以與您聯繫。

我的問題是,這已被驗證爲一個錯誤,如果是的話它被修復了嗎? 我問的原因是我似乎有一個類似的問題。 基本上我的模型看起來是這樣的:

class Wheel < ActiveRecord::Base 
    belongs_to :vehicle 
end 

# Let's say it has a string attribute called serial_number 
class Vehicle < ActiveRecord::Base 
    has_many :wheels 
end 

class Car < Vehicle 
end 

class Truck < Vehicle 
end 

# etc 

# Without abstract_class = true this query is very slow (up to 10x slower) 
# With abstract_class = true it's suddenly very fast 
Wheel.include(:vehicle).sort_by("vehicles.serial_number ASC") 

這個問題似乎與您上述的錯誤嗎? 對我來說,看起來可能它沒有預先加載車輛協會,但說實話我並不覺得我足夠了解Rails/ActiveRecord對它的任何說明。

(我測試過這種使用Rails 3.2.8)

現在我可以解決這一問題,但增加abstract_class = true來車,但它會如何影響應用程序的其他部分?看看abstract_class = true似乎會禁用STI,並指出每個子類都應該有它自己的表,但是在測試時我沒有遇到任何這樣的問題。

+1

是的,這似乎是由同一個錯誤引起的。據我所知,這個錯誤從來沒有被修復,驗證過,甚至沒有正式報告過,所以3.2.8版本可能仍然存在。該錯誤阻止了sti數據的正確預加載,這正是您的查詢所做的,並解釋了速度差異。我沒有看到Rails 2.3.14中的任何東西都會讓「abstract_class = true」成爲sti類的一個問題,並且我一直在運行一個生產應用程序,該應用程序可以在幾個月內實現該解決方案,而不會出現任何問題。我不能說在Rails 3.2.8中有什麼改變。 – 2012-10-22 19:42:34