2

ここではRails 2.3.14を使用していますが、この特定のバージョンに限定されない問題が発生する可能性があります。これはすべて、Rails 3 にまだ存在するアソシエーションと熱心な読み込み機能であり、2.3.14 よりもかなり前から存在していました。私はレール 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です。一方、CarTruckは のサブクラスですVehicle。車両用のデータベースはSTIで設定されているのでCars、 と両方ともテーブルTrucksに対応しています(列微分器付き)。vehiclestype

Drivable次に、すべての最下位レベルのドメイン オブジェクト ( CarTruck、 ) にモジュールを含めていることに注意してください。GolfBallこれは次のようになります (実際のシステムでは、このモジュールは、特定のものに基づいて物事を設定するなど、さらに多くのことを行います。ドメイン オブジェクト):

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を使用しています (たとえば、クラスをインクルードすると with になります)。これらの参照されるクラス (など) にはそれぞれに必要な特定のビジネス ロジックが含まれているためです。協会の使用。:class_nameCarhas_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    ...

Vehicle車両がSTIであるためとはdriveable_type対照的です。Carここまでは順調ですね。ここで、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タイプの場合はCarSTI 基底クラス名を探すべき場所を探します。Vehicle

これを修正する方法はありますか?

4

1 に答える 1

2

Railsのソースコードを何時間も読んだ後、私はこれを理解したと確信しています(もちろん、何かを読み間違えている可能性があります). Rails 2.3.9 で導入され、修正されなかった Rails 2.3.x ブランチのバグのようです。この Rails 2.3 バグ レポートから始まりました。

ポリモーフィックな belongs_to の下にネストされたアソシエーションをプリロードするためのクエリが間違って作成されました

実際、これは有効なバグであり、Rails 2.3.9 のこのコミットによって修正されました。

ネストされたポリモーフィックな has_one アソシエーションの熱心な読み込みを修正

ただし、このコミットにより、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 より前では、eager-loaded ポリモーフィック アソシエーションの場合、常に(ポリモーフィック タイプの列)self.base_class.sti_nameとして使用されていたことがわかります。parent_type質問の例に従うと、親の型列がdriveable_typeになり、Car.base_classisVehicleであるため、 on に正しく一致することを意味しdriveable_type = 'Vehicle'ます。

ただし、上記のコミットから始めて、アソシエーション ホルダーがabstract_class. これは単純なバグのようです。これは、アソシエーション ホルダーが抽象クラスまたはsti サブクラスのいずれかである場合、 を に設定する必要があるように設定parent_typeする必要がありself.base_class.sti_nameます。

幸いなことに、これを回避する簡単な方法があります。abstract_class = trueこれはどこのドキュメントでも言及されていませんが、ポリモーフィックな belongs_to 関連付けを保持する sti サブクラスを設定することで解決できるようです。質問の例では、次のようになります。

class Car < Vehicle
    abstract_class = true
    include Driveable
end

class Truck < Vehicle
    abstract_class = true
    include Driveable
end

Railsソースの全文検索をabstract_class実行すると、意図しない/望ましくない結果が生じるとは思われない設定があり、実際のアプリでの最初のテストでは、問題が解決したようです。

于 2012-07-06T22:53:23.277 に答える