web-dev-qa-db-ja.com

Eager Load Polymorphic

Rails 3.2を使用すると、このコードの何が問題になっていますか?

@reviews = @user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe')

このエラーが発生します:

ポリモーフィックな関連付けを熱心にロードできません:reviewable

reviewable.shop_type = ?条件を削除すると、機能します。

reviewable_typeおよびreviewable.shop_type(実際にはshop.shop_type)に基づいてフィルタリングするにはどうすればよいですか?

97
Victor

私の推測では、モデルは次のようになります。

class User < ActiveRecord::Base
  has_many :reviews
end

class Review < ActiveRecord::Base
  belongs_to :user
  belongs_to :reviewable, polymorphic: true
end

class Shop < ActiveRecord::Base
  has_many :reviews, as: :reviewable
end

いくつかの理由により、そのクエリを実行できません。

  1. ActiveRecordは、追加情報なしでは結合を構築できません。
  2. レビュー可能というテーブルはありません

この問題を解決するには、ReviewShopの関係を明示的に定義する必要があります。

class Review < ActiveRecord::Base
   belongs_to :user
   belongs_to :reviewable, polymorphic: true
   # For Rails < 4
   belongs_to :shop, foreign_key: 'reviewable_id', conditions: "reviews.reviewable_type = 'Shop'"
   # For Rails >= 4
   belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
   # Ensure review.shop returns nil unless review.reviewable_type == "Shop"
   def shop
     return unless reviewable_type == "Shop"
     super
   end
end

次に、次のようにクエリできます。

Review.includes(:shop).where(shops: {shop_type: 'cafe'})

テーブル名はshopsではなくreviewableであることに注意してください。データベースにreviewableと呼ばれるテーブルがあってはなりません。

joinReviewの間でShopを明示的に定義するよりも簡単で柔軟だと思います。関連フィールドによるクエリに加えて負荷を積極的にかけることができるからです。

これが必要な理由は、複数のテーブルが結合のもう一方の端を表すため、ActiveRecordはレビュー可能な単独に基づいて結合を構築できないためです。SQLは、私が知る限り、列に。追加の関係を定義するbelongs_to :shop、ActiveRecordに参加を完了するために必要な情報を提供しています。

192
Sean Hill

ActiveRecord :: EagerLoadPolymorphicErrorが発生した場合、includeseager_loadポリモーフィックな関連付けがpreloadでのみサポートされている場合。次のドキュメントにあります: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html

したがって、多態的な関連付けには常にpreloadを使用してください。これには1つの注意点があります。where句でポリモーフィックアソシエーションを照会することはできません(ポリモーフィックな関連付けは複数のテーブルを表すため、これは理にかなっています)。

5
seanmorton
@reviews = @user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)

WHEREでSQLフラグメントを使用している場合、関連付けを結合するには参照が必要です。

0
un_gars_la_cour

優れた補遺の答えとして、何らかの理由で使用しているクエリにモデルのテーブルが含まれておらず、未定義のテーブルエラーが発生している場合は、関連付けに_:include_を指定することもできます。

そのようです:

_belongs_to :shop, 
           foreign_key: 'reviewable_id', 
           conditions: "reviews.reviewable_type = 'Shop'",
           include: :reviews
_

_:include_オプションを使用しない場合、上記の例で関連付け_review.shop_にアクセスするだけの場合、UndefinedTableエラーが発生します(Rails 3ではなく4でテスト)関連付けはSELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' )を実行します。

_:include_オプションは、代わりに強制的にJOINを実行します。 :)

0