Carga rápida polimórfica

104

Usando o Rails 3.2, o que há de errado com esse código?

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

Isso gera este erro:

Não é possível carregar avidamente a associação polimórfica: revisável

Se eu remover a reviewable.shop_type = ?condição, funciona.

Como posso filtrar com base no reviewable_typee reviewable.shop_type(que é na verdade shop.shop_type)?

Vencedor
fonte

Respostas:

207

Meu palpite é que seus modelos são assim:

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

Você não pode fazer essa consulta por vários motivos.

  1. ActiveRecord não é capaz de construir a junção sem informações adicionais.
  2. Não há nenhuma tabela chamada revisável

Para resolver esse problema, você precisa definir explicitamente a relação entre Reviewe 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

Então você pode consultar assim:

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

Observe que o nome da tabela é shopse não reviewable. Não deve haver uma tabela chamada revisável no banco de dados.

Acredito que isso seja mais fácil e flexível do que definir explicitamente o joinentre Reviewe, Shoppois permite que você carregue com antecedência, além de consultar por campos relacionados.

A razão pela qual isso é necessário é que ActiveRecord não pode construir uma junção baseada apenas em revisável, uma vez que várias tabelas representam a outra extremidade da junção, e o SQL, até onde eu sei, não permite que você junte uma tabela nomeada pelo valor armazenado em uma coluna. Ao definir o relacionamento extra belongs_to :shop, você está fornecendo ao ActiveRecord as informações de que precisa para concluir a associação.

Sean Hill
fonte
6
Na verdade eu acabei usando isso sem declarar mais nada:@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
Isso é porque :reviewableé Shop. As fotos pertencem à loja.
Victor
6
funcionou no rails4, mas dará um aviso de depreciação, ele disse que deveria usar um estilo como has_many: spam_comments, -> {where spam: true}, class_name: 'Comment'. Então, em rails4, será belongs_to: shop, -> {where ("reviews.reviewable_type = 'Shop'")}, foreign_key: 'reviewable_id'.Mas tenha cuidado, Review.includes (: shop) irá gerar um erro, deve anexar pelo menos uma cláusula where.
raykin,
49
Há também foreign_type, que funcionou para mim para um problema semelhante:belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
A5308Y
14
Ao carregar, reviewsincluindo o carregamento antecipado do shopcódigo associado , Review.includes(:shop)a definição belongs_to belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id' gera um erro dizendo missing FROM-clause entry for table "reviews". Eu belongs_to :shop, -> { joins(:reviews) .where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
corrigi-
12

Se você obtiver um ActiveRecord :: EagerLoadPolymorphicError, é porque includesdecidiu chamar eager_loadquando as associações polimórficas são suportadas apenas por preload. Está na documentação aqui: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html

Portanto, sempre use preloadpara associações polimórficas. Há uma advertência para isso: você não pode consultar a associação polimórfica nas cláusulas where (o que faz sentido, já que a associação polimórfica representa várias tabelas).

Seanmorton
fonte
Vejo que esse é o único método que não está documentado nos guias: guias.rubyonrails.org/…
MSC de
0
@reviews = @user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)

Quando você está usando fragmentos SQL com WHERE, referências são necessárias para ingressar em sua associação.

un_gars_la_cour
fonte
0

Como adendo a resposta no topo, que é excelente, você também pode especificar :includena associação se por algum motivo a consulta que você está usando não inclui a tabela do modelo e você está obtendo erros de tabela indefinida.

Igual a:

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

Sem a :includeopção, se você simplesmente acessar a associação review.shopno exemplo acima, obterá um erro UndefinedTable (testado no Rails 3, não 4) porque a associação servirá SELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' ).

A :includeopção irá forçar um JOIN. :)

Stewart Mckinney
fonte
5
Chave desconhecida:: condições. As chaves válidas são:: class_name,: class,: foreign_key,: validate,: autosave,: dependente,: primary_key,: inverse_of,: required,: foreign_type,: polymorphic,: touch,: counter_cache
Bengala