Charge impatiente polymorphe

104

En utilisant Rails 3.2, quel est le problème avec ce code?

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

Cela soulève cette erreur:

Impossible de charger avec empressement l'association polymorphe: révisable

Si je supprime le reviewable.shop_type = ? condition, cela fonctionne.

Comment puis-je filtrer en fonction du reviewable_typeet reviewable.shop_type(qui est en fait shop.shop_type)?

Victor
la source

Réponses:

207

Je suppose que vos modèles ressemblent à ceci:

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

Vous ne pouvez pas effectuer cette requête pour plusieurs raisons.

  1. ActiveRecord ne peut pas créer la jointure sans informations supplémentaires.
  2. Il n'y a pas de tableau appelé révisable

Pour résoudre ce problème, vous devez définir explicitement la relation entre Reviewet 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

Ensuite, vous pouvez interroger comme ceci:

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

Notez que le nom de la table est shopset non reviewable. Il ne doit pas y avoir de table appelée révisable dans la base de données.

Je pense que cela est plus facile et plus flexible que de définir explicitement l' joinintervalle Reviewet Shopcar cela vous permet de charger rapidement en plus d'interroger des champs connexes.

La raison pour laquelle cela est nécessaire est qu'ActiveRecord ne peut pas créer une jointure basée uniquement sur révisable, car plusieurs tables représentent l'autre extrémité de la jointure et SQL, pour autant que je sache, ne vous permet pas de rejoindre une table nommée par la valeur stockée dans une colonne. En définissant la relation supplémentaire belongs_to :shop, vous donnez à ActiveRecord les informations dont il a besoin pour terminer la jointure.

Sean Hill
la source
6
En fait, j'ai fini par utiliser ceci sans rien déclarer de plus:@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
1
C'est parce que :reviewablec'est Shop. Les photos appartiennent à la boutique.
Victor
6
a fonctionné dans rails4, mais donnera un avertissement de dépréciation, il a dit devrait utiliser un style comme has_many: spam_comments, -> {where spam: true}, class_name: 'Comment'. Donc, dans rails4, appartiendra à: shop, -> {where ("reviews.reviewable_type = 'Shop'")}, Foreign_key: 'reviewable_id'.Mais attention, Review.includes (: shop) générera une erreur, il doit ajouter au bail une clause where.
raykin
49
Il y a aussi Foreign_type, qui a fonctionné pour moi pour un problème similaire:belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
A5308Y
14
Lors du chargement, reviewsy compris le chargement hâtif du shopcode d'utilisation associé, Review.includes(:shop)la définition appartient_to belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id' renvoie une erreur en disant missing FROM-clause entry for table "reviews". Je l'ai corrigé en mettant à jour la définition de Splend_to de la manière suivante: belongs_to :shop, -> { joins(:reviews) .where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
Jignesh Gohel
12

Si vous obtenez une ActiveRecord :: EagerLoadPolymorphicError, c'est parce que j'ai includesdécidé d'appelereager_load lorsque les associations polymorphes ne sont prises en charge que par preload. C'est dans la documentation ici: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html

Utilisez donc toujours preloadpour les associations polymorphes. Il y a une mise en garde à cela: vous ne pouvez pas interroger l'assocition polymorphe dans les clauses where (ce qui est logique, car l'association polymorphe représente plusieurs tables.)

seanmorton
la source
Je vois que c'est la seule méthode qui n'est pas documentée dans les guides: guides.rubyonrails.org
MSC
0
@reviews = @user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)

Lorsque vous utilisez des fragments SQL avec WHERE, des références sont nécessaires pour rejoindre votre association.

un_gars_la_cour
la source
0

En complément la réponse en haut, ce qui est excellent, vous pouvez également spécifier :include sur l'association si pour une raison quelconque la requête que vous utilisez n'inclut pas la table du modèle et vous obtenez des erreurs de table non définies.

Ainsi:

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

Sans l' :includeoption, si vous accédez simplement à l'association review.shopdans l'exemple ci-dessus, vous obtiendrez une erreur UndefinedTable (testée dans Rails 3, pas 4) car l'association fera l'affaire SELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' ).

L' :includeoption forcera un JOIN à la place. :)

Stewart Mckinney
la source
5
Clé inconnue:: conditions. Les clés valides sont:: nom_classe,: classe,: clé_trangère,: validation,: sauvegarde automatique,: dépendante,: clé_principale,: inverse_of,: obligatoire,: type_étranger,: polymorphe,: touch,: counter_cache
Bengala