Pré-carregando has_many com escopo: por meio de associações

Recentemente, eu li o artigo muito bom de Justin Weiss sobre Como pré-carregar escopos Rails . A essência é esta – se você tiver:

class Product < ActiveRecord::Base
has_many
:ratings
end

e

class Rating < ActiveRecord::Base
belongs_to
:product
end

em vez de ter um scopeinterior Ratingcomo este:

scope :positive, -> { where 'score > 3.0'}

você pode ter uma associação com escopo dentro Product

has_many :positive_ratings, -> { where 'score > 3.0'}, class_name: 'Rating'

A razão para isso é que, no primeiro caso, você chamaria as avaliações positivas para um produto como este:

product.ratings.positive

e no segundo caso assim:

product.positive_ratings

Isso também permite que você os carregue rapidamente assim:

Product.includes(:positive_ratings).

o que não é possível no primeiro caso.

Uma reviravolta ainda mais interessante: e se você tiver uma associação entre dois modelos e o modelo também usar o padrão de Herança de tabela única ?has_many :throughthrough

Vamos ver na prática:

class User < ActiveRecord::Base
has_many
:activities, inverse_of: :user
has_many
:products, through: :activities
end

class Activity < ActiveRecord::Base
belongs_to
:user, inverse_of: :activities
belongs_to
:product, inverse_of: :activities
end

class View < Activity; end

class Save < Activity; end

class Product < ActiveRecord::Base
has_many
:activities, inverse_of: :product
has_many
:users, through: :activities
end

E se você quiser obter apenas os produtos salvos para um usuário? Você pode adicionar o seguinte método a User:

def saved_products
products
.includes(:activities).where(activities: {type: 'Save'})
end

No entanto, você não pode pré-carregar os produtos salvos e você teria o problema, ao carregar alguns usuários e exibir seus produtos salvos. Vamos consertar isso.N+1

Primeiro, vamos adicionar a associação com escopo entre Usere Activitypara User:

has_many :saves, -> { where type: 'Save' }, class_name: 'Activity'

Isso nos permitirá carregar antecipadamente nossos produtos salvos como este:

User.includes(saves: :product).first

Para que seja ainda mais conveniente, você precisa dar o próximo passo e adicionar a associação com escopo entre Usere Product:

has_many :saved_products, through: :saves, source: :product

A etapa principal aqui é adicionar a sourceopção. Isto:

Especifica o nome da associação de origem usado por várias: através de consultas. Use-o apenas se o nome não puder ser inferido da associação. tem muitos: assinantes, por meio de:: assinaturas procurarão: assinantes ou: assinante em Assinatura, a menos que um: source seja fornecido.

Uma vez Activityque não tem uma associação, chamada ‘VistoProduto’, você precisa especificar o nome correto usando source.

Agora você pode apenas ligar para:

User.includes(:saved_products).first

Feliz pré-carregamento!