126

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)?

Ilya Lavrov
  • 2,810
  • 3
  • 20
  • 37
Victor
  • 13,010
  • 18
  • 83
  • 146

6 Answers6

239

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.

Andrew Hampton
  • 1,632
  • 3
  • 20
  • 29
Sean Hill
  • 14,978
  • 2
  • 50
  • 56
  • 7
    Actually I ended up using this without declaring anything more: `@reviews = @user.reviews.joins("INNER JOIN shops ON (reviewable_type = 'Shop' AND shops.id = reviewable_id AND shops.shop_type = '" + type + "')").includes(:user, :reviewable => :photos)` – Victor Apr 22 '13 at 16:53
  • You mean my method? Yeah, it works. Yours works too, but I am lazy to declare too many things. :P – Victor Apr 22 '13 at 16:59
  • Yeah, I meant your method. Nice! I'm really surprised that the `:reviewable => :photos` worked like that. – Sean Hill Apr 22 '13 at 17:01
  • 1
    That's because `:reviewable` is `Shop`. Photos belongs to Shop. – Victor Apr 22 '13 at 17:08
  • 6
    worked in rails4,but will give a deprecation warning, it said should use style like has_many :spam_comments, -> { where spam: true }, class_name: 'Comment'. So in rails4, will be belongs_to :shop, -> {where( "reviews.reviewable_type = 'Shop'")}, foreign_key: 'reviewable_id'.But be care, Review.includes(:shop) will raise error, it must append at lease one where clause. – raykin Apr 09 '14 at 10:29
  • 1
    Thanks, raykin. I've finally gotten back around to updating the answer. – Sean Hill Jun 30 '14 at 23:04
  • The reason that `Review.includes(:shop)` will raise an error is because before, the answer included a SQL snippet for the condition. The solution is to no longer use a SQL snippet, like the updated answer above, or to do `Review.includes(:shop).where('shops.shop_type = ?', 'cafe').references(:shop)`. – Sean Hill Jun 30 '14 at 23:09
  • 63
    There is also foreign_type, which worked for me for a similar problem: `belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'` – A5308Y Mar 18 '15 at 15:12
  • That's a good point. However, wouldn't `source_type` be better here instead of `foreign_type`? The docs state, ":source_type - Specifies type of the source association used by has_many :through queries where the source association is a polymorphic belongs_to." `foreign_type` seems to allow you to define the relation name, i.e. `taggable`. – Sean Hill Mar 19 '15 at 13:42
  • Hey could you please help me on http://stackoverflow.com/questions/33421083/how-to-solve-this-n1-query-for-2-associations? I'm unable to follow your instructions here. – Amit Joki Nov 03 '15 at 03:13
  • 1
    :foreign_type doc looks unclear to me. It says to specify a column and in the example shown by @A5308Y uses the column's value. Quoting below the belongs_to :foreign_type doc from http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to > Specify the column used to store the associated object's type, if this is a polymorphic association. By default this is guessed to be the name of the association with a “_type” suffix. So a class that defines a belongs_to :taggable, polymorphic: true association will use “taggable_type” as the default :foreign_type. – Jignesh Gohel Apr 13 '16 at 08:47
  • 20
    When loading `reviews` including eager loading the associated `shop` using code `Review.includes(:shop)` the belongs_to definition `belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'` throws error saying `missing FROM-clause entry for table "reviews"`. I fixed it by updating belongs_to definition in following manner: `belongs_to :shop, -> { joins(:reviews) .where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id' ` – Jignesh Gohel Apr 13 '16 at 09:47
  • Thanks so much @JiggneshhGohel, the `joins` was the missing piece for me. As soon as I read your comment it was so obvious. – supremebeing7 Dec 15 '16 at 05:30
  • 1
    Man, how did this work for anyone without @JiggneshhGohel's crucial missing piece? – Grant Birchmeier Apr 20 '17 at 22:16
  • However, @A5308Y's comment is better by far, and appears to do the same thing. – Grant Birchmeier Apr 20 '17 at 22:20
  • 5
    I tried this, but it surfaced a very interesting bug. If there is a Shop and a User with the same database ID, then calling `review.shop` on a review with `reviewable_type` set to `User` could return a completely unrelated Shop rather than `nil`. In some circumstances this can be a very serious data leak because a user may have permission to access `review` but not the Shop returned by `review.shop`. – phylae Dec 15 '17 at 00:08
  • 3
    @phylae we ran into the exact same bug and found the best workaround was to define a `shop` function that would return nil if the reviewable_type != "Shop", otherwise just call super. ```ruby def shop return unless reviewable_type == "Shop" super end ``` – Andrew Hampton Dec 15 '17 at 13:56
  • I'm afraid this answer is not completely acceptable. It won't work unless you put `where(shops: {shop_type: 'cafe'})` in your query. I found no easy solution for this for myself. – serggl Oct 07 '14 at 18:51
  • @AndrewHampton I know, I wrote it :) – phylae Dec 21 '17 at 04:15
  • pretty sure foreign_type should be the name of the column not the string version of the class (that would be class_name). – pixelearth Jun 18 '18 at 21:15
  • 2
    Need to include `optional: true` to the `belongs_to` definition if you want to be able to use the polymorphic association with more than just one model. – Simon L. Brazell Aug 10 '20 at 08:36
  • tried `foreign_type` but it seems to make no difference, does not prevent the problem of getting a different type of reviewable outlined by @phylae , I settled on `belongs_to :course_application, -> {joins(:messages).where(comm_transmissions: {context_type: 'CourseApplication'})}, foreign_key: 'context_id', foreign_type: 'context_type' def course_application return unless context_type == "CourseApplication" super end` Sorry this is my own situation where `CourseApplication` is one of the possible polymorphic relations for `context` – ryan2johnson9 Sep 07 '20 at 00:17
  • 1
    @GrantBirchmeier re `However, @A5308Y's comment is better by far, and appears to do the same thing.` did you test it properly because for me it had the potential of returning a relation which should have been nil, i.e. context_type "User" but returned a Shop as it shared an id – ryan2johnson9 Sep 07 '20 at 00:33
  • Hey @Sean Hill, I want to use the same exact setup but I cant figure out how the Reviews migration table looks like, can you please share it? – Dev Jan 15 '21 at 20:29
24

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.)

seanmorton
  • 371
  • 2
  • 2
  • I see that's the one method that's not documented in the guides: https://guides.rubyonrails.org/active_record_querying.html#retrieving-objects-from-the-database – MSC Aug 22 '18 at 22:11
6

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:!

Tyler Klose
  • 113
  • 1
  • 2
1

This did the work for me

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

  • >> Transaction.joins(:from_bank).where(from_bank: Bank.all) An error occurred when inspecting the object: # – Dorian Mar 15 '23 at 04:24
0

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. :)

Community
  • 1
  • 1
  • 9
    Unknown key: :conditions. Valid keys are: :class_name, :class, :foreign_key, :validate, :autosave, :dependent, :primary_key, :inverse_of, :required, :foreign_type, :polymorphic, :touch, :counter_cache – Miguel Peniche Mar 07 '16 at 08:11
-1
@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.

un_gars_la_cour
  • 259
  • 1
  • 4