Полиморфная загрузка

104

Что не так с этим кодом при использовании Rails 3.2?

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

Это вызывает эту ошибку:

Не могу сразу загрузить полиморфную ассоциацию: доступен для просмотра

Если я уберу reviewable.shop_type = ?условие, оно сработает.

Как я могу фильтровать по reviewable_typeи reviewable.shop_type(что на самом деле shop.shop_type)?

Виктор
источник

Ответы:

207

Я предполагаю, что ваши модели выглядят так:

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. Нет таблицы с названием для проверки

Чтобы решить эту проблему, вам необходимо явно определить связь между Reviewи 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

Тогда вы можете запросить вот так:

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

Обратите внимание, что имя таблицы - shopsа не reviewable. В базе данных не должно быть таблицы, которая может быть проверена.

Я считаю, что это проще и гибче, чем явное определение joinмежду Reviewи, Shopпоскольку это позволяет вам выполнять загрузку в дополнение к запросам по связанным полям.

Причина, по которой это необходимо, заключается в том, что ActiveRecord не может построить соединение на основе только проверяемого, поскольку несколько таблиц представляют другой конец соединения, а SQL, насколько мне известно, не позволяет вам присоединиться к таблице, названной сохраненным значением в столбце. Определяя дополнительную связь belongs_to :shop, вы даете ActiveRecord информацию, необходимую для завершения соединения.

Шон Хилл
источник
6
На самом деле я закончил тем, что использовал это, ничего больше не объявляя:@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)
Виктор
1
Это потому что :reviewableесть Shop. Фотографии принадлежат Магазин.
Виктор
6
работал в rails4, но выдает предупреждение об устаревании, в нем говорится, что следует использовать стиль, например has_many: spam_comments, -> {where spam: true}, class_name: 'Comment'. Итак, в rails4 будет own_to: shop, -> {where ("reviews.reviewable_type = 'Shop'")}, foreign_key: 'reviewable_id'. Но будьте осторожны, Review.includes (: shop) вызовет ошибку, он должен добавить в аренду один пункт where.
райкин 09
49
Есть также foreign_type, который помог мне решить аналогичную проблему:belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
A5308Y
14
При загрузке, reviewsвключая нетерпеливую загрузку связанного shopс использованием кода, Review.includes(:shop) определение own_to belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id' выдает сообщение об ошибке missing FROM-clause entry for table "reviews". Я исправил это, обновив определение own_to следующим образом: belongs_to :shop, -> { joins(:reviews) .where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
Jignesh Gohel
12

Если вы получили ActiveRecord :: EagerLoadPolymorphicError, это связано с тем, что было includesрешено вызывать, eager_loadкогда полиморфные ассоциации поддерживаются только preload. Это в документации здесь: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html

Поэтому всегда используйте preloadдля полиморфных ассоциаций. Здесь есть одно предостережение: вы не можете запрашивать полиморфную ассоциацию в предложениях where (что имеет смысл, поскольку полиморфная ассоциация представляет несколько таблиц).

Seanmorton
источник
Я вижу, что это единственный метод, который не описан
MSC
0
@reviews = @user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)

Когда вы используете фрагменты SQL с WHERE, ссылки необходимы для присоединения к вашей ассоциации.

un_gars_la_cour
источник
0

В качестве дополнения ответ вверху, который превосходен, вы также можете указать :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 вместо этого. :)

Стюарт Маккинни
источник
5
Неизвестный ключ:: условия. Допустимые ключи:: class_name,: class,: foreign_key,: validate,: autosave,: independent,: primary_key,: inverse_of,: required,: foreign_type,: polymorphic,: touch,: counter_cache
Бенгала,