Eager load polymorphic

Ruby on-RailsRuby on-Rails-3Activerecord

Ruby on-Rails Problem Overview


Using Rails 3.2, what's wrong with this code?

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

It raises this error:

> Can not eagerly load the polymorphic association :reviewable

If I remove the reviewable.shop_type = ? condition, it works.

How can I filter based on the reviewable_type and reviewable.shop_type (which is actually shop.shop_type)?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

My guess is that your models look like this:

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

You are unable to do that query for several reasons.

  1. ActiveRecord is unable to build the join without additional information.
  2. There is no table called reviewable

To solve this issue, you need to explicitly define the relationship between Review and Shop.

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

Then you can query like this:

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

Notice that the table name is shops and not reviewable. There should not be a table called reviewable in the database.

I believe this to be easier and more flexible than explicitly defining the join between Review and Shop since it allows you to eager load in addition to querying by related fields.

The reason that this is necessary is that ActiveRecord cannot build a join based on reviewable alone, since multiple tables represent the other end of the join, and SQL, as far as I know, does not allow you join a table named by the value stored in a column. By defining the extra relationship belongs_to :shop, you are giving ActiveRecord the information it needs to complete the join.

Solution 2 - Ruby on-Rails

If you get an ActiveRecord::EagerLoadPolymorphicError, it's because includes decided to call eager_load when polymorphic associations are only supported by preload. It's in the documentation here: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html

So always use preload for polymorphic associations. There is one caveat for this: you cannot query the polymorphic assocition in where clauses (which makes sense, since the polymorphic association represents multiple tables.)

Solution 3 - Ruby on-Rails

This did the work for me

  belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'

Solution 4 - Ruby on-Rails

Not enough reputation to comment to extend the response from Moses Lucas above, I had to make a small tweak to get it to work in Rails 7 as I was receiving the following error:

ArgumentError: Unknown key: :foreign_type. Valid keys are: :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :autosave, :required, :touch, :polymorphic, :counter_cache, :optional, :default

Instead of belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'

I went with belongs_to :shop, class_name: 'Shop', foreign_key: 'reviewable_id'

The only difference here is changing foreign_type: to class_name:!

Solution 5 - Ruby on-Rails

As an addendum the answer at the top, which is excellent, you can also specify :include on the association if for some reason the query you are using is not including the model's table and you are getting undefined table errors.

Like so:

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

Without the :include option, if you merely access the association review.shop in the example above, you will get an UndefinedTable error ( tested in Rails 3, not 4 ) because the association will do SELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' ).

The :include option will force a JOIN instead. :)

Solution 6 - Ruby on-Rails

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

When you are using SQL fragments with WHERE, references is necessary to join your association.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVictorView Question on Stackoverflow
Solution 1 - Ruby on-RailsSean HillView Answer on Stackoverflow
Solution 2 - Ruby on-RailsseanmortonView Answer on Stackoverflow
Solution 3 - Ruby on-RailsMoses LucasView Answer on Stackoverflow
Solution 4 - Ruby on-RailsTyler KloseView Answer on Stackoverflow
Solution 5 - Ruby on-RailsStewart MckinneyView Answer on Stackoverflow
Solution 6 - Ruby on-Railsun_gars_la_courView Answer on Stackoverflow