【问题标题】:Rails STI models with polymorphic assocation, eager loading query references the wrong class type具有多态关联的 Rails STI 模型,急切加载查询引用了错误的类类型
【发布时间】:2012-07-06 18:16:18
【问题描述】:

我在这里使用的是 Rails 2.3.14,但我遇到的问题可能并不局限于这个特定版本。这是 Rails 3 中仍然存在的所有关联和急切加载功能,并且在 2.3.14 之前就已经存在。我正在从 rails 2.3.8 升级,我没有遇到下面描述的问题。

下面的代码是基于更复杂的生产系统的模型。我概述的类/模块方案是这样设置的,这是有原因的。实际上,我在这个模型中包含了比演示问题所需的更多细节,希望使系统的整体结构更加清晰。

假设我有几个可以“驱动”的域对象,包括车辆(汽车/卡车)和高尔夫球。对于这些事情,我都有一个 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 =&gt; "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。到现在为止还挺好。现在我打开一个 Rails 控制台并执行一个简单的命令来获取一个 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,就像在非急切加载情况下一样。

知道如何解决这个问题吗?

【问题讨论】:

    标签: ruby-on-rails polymorphic-associations eager-loading sti


    【解决方案1】:

    在阅读了几个小时的 rails 源代码之后,我很确定我明白了这一点(当然,我可能误读了某些内容)。这似乎是 Rails 2.3.9 中引入的 Rails 2.3.x 分支中的一个错误,并且从未修复过。它始于这份 Rails 2.3 错误报告:

    Query built wrong for preloading associations nested under polymorphic belongs_to

    确实这是一个有效的错误,并且在 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_name 作为 parent_type(这是多态类型列)。按照问题中的示例,这意味着父类型列将是driveable_type,并且由于Car.base_classVehicle,它将正确匹配driveable_type = 'Vehicle'

    然而,从上面的提交开始,它只有在关联持有者是abstract_class 时才能正确执行。这似乎是一个直接的错误;如果关联持有者是抽象 sti 子类,则应该设置它,它应该将 parent_type 设置为 self.base_class.sti_name

    幸运的是,有一种解决此问题的简单方法。虽然它在任何地方的文档中都没有提到这一点,但似乎可以通过在 sti 子类上设置 abstract_class = true 来解决这个问题,该子类包含多态 belongs_to 关联。在问题的示例中,这将是:

    class Car < Vehicle
        abstract_class = true
        include Driveable
    end
    
    class Truck < Vehicle
        abstract_class = true
        include Driveable
    end
    

    abstract_class的rails源进行全文搜索,设置似乎不会产生任何意外/不希望的后果,并且在使用真实应用程序的初始测试中,它似乎已经解决了问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-21
      • 2012-08-08
      相关资源
      最近更新 更多