Como exibir registros exclusivos de um relacionamento has_many?

106

Estou me perguntando qual é a melhor maneira de exibir registros exclusivos de um has_many, por meio de relacionamento no Rails3.

Tenho três modelos:

class User < ActiveRecord::Base
    has_many :orders
    has_many :products, :through => :orders
end

class Products < ActiveRecord::Base
    has_many :orders
    has_many :users, :through => :orders
end

class Order < ActiveRecord::Base
    belongs_to :user, :counter_cache => true 
    belongs_to :product, :counter_cache => true 
end

Digamos que eu queira listar todos os produtos que um cliente pediu em sua página do programa.

Eles podem ter pedido alguns produtos várias vezes, então estou usando counter_cache para exibir em ordem decrescente de classificação, com base no número de pedidos.

Mas, se eles solicitaram um produto várias vezes, preciso garantir que cada produto seja listado apenas uma vez.

@products = @user.products.ranked(:limit => 10).uniq!

funciona quando há vários registros de pedido para um produto, mas gera um erro se um produto foi pedido apenas uma vez. (classificado é uma função de classificação personalizada definida em outro lugar)

Outra alternativa é:

@products = @user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")

Não tenho certeza de estar na abordagem certa aqui.

Alguém mais abordou isso? Quais problemas você encontrou? Onde posso encontrar mais informações sobre a diferença entre .unique! e DISTINTO ()?

Qual a melhor forma de gerar uma lista de registros únicos através de um has_many, através de relacionamento?

obrigado

Andy Harvey
fonte

Respostas:

235

Você tentou especificar a opção: uniq na associação has_many:

has_many :products, :through => :orders, :uniq => true

Da documentação do Rails :

:uniq

Se verdadeiro, as duplicatas serão omitidas da coleção. Útil em conjunto com: through.

ATUALIZAÇÃO PARA TRILHOS 4:

No Rails 4, has_many :products, :through => :orders, :uniq => trueestá obsoleto. Em vez disso, você deve escrever agora has_many :products, -> { distinct }, through: :orders. Consulte a seção distinta para has_many:: por meio de relacionamentos na documentação do ActiveRecord Associations para obter mais informações. Agradecimentos a Kurt Mueller por apontar isso em seu comentário.

mbreining
fonte
6
A diferença não é tanto se você remove duplicatas no modelo ou controlador, mas em vez disso, você usa a opção: uniq em sua associação (como mostrado na minha resposta) ou o SQL DISTINCT stmt (por exemplo, has_many: products,: through =>: orders ,: select => "DISTINCT products. *). No primeiro caso, TODOS os registros são buscados e o rails remove as duplicatas para você. No último caso, apenas os registros não duplicados são buscados no banco de dados para que possa oferecer um melhor desempenho se você tiver um conjunto de resultados grande.
mbreining
68
No Rails 4, has_many :products, :through => :orders, :uniq => trueestá obsoleto. Em vez disso, você deve escrever agora has_many :products, -> { uniq }, through: :orders.
Kurt Mueller
8
Observe que -> {uniq} neste sentido é apenas um alias para -> {distinto} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Ocorre em SQL, não em ruby
engineerDave
5
Se você receber um conflito com as cláusulas DISTINCTe ORDER BY, você sempre pode usarhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
obrigado @fagiani e se o seu modelo tiver uma coluna json e você usar psql, fica mais complicado ainda e você tem que fazer algo do has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationcontráriorails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Note que uniq: truefoi removido das opções válidas para has_manyRails 4.

No Rails 4 você deve fornecer um escopo para configurar este tipo de comportamento. Os escopos podem ser fornecidos por meio de lambdas, assim:

has_many :products, -> { uniq }, :through => :orders

O guia do rails cobre esta e outras maneiras de usar escopos para filtrar as consultas de sua relação, role para baixo até a seção 4.3.3:

http://guides.rubyonrails.org/association_basics.html#has-many-association-reference

Yoshiki
fonte
4
No Rails 5.1, continuei recebendo undefined method 'except' for #<Array...e foi porque usei no .uniqlugar de.distinct
stevenspiel
5

Você poderia usar group_by. Por exemplo, tenho um carrinho de compras de galeria de fotos para o qual desejo que os itens sejam classificados de acordo com a foto (cada foto pode ser pedida várias vezes e em impressões de tamanhos diferentes). Em seguida, retorna um hash com o produto (foto) como a chave e cada vez que ele foi pedido pode ser listado no contexto da foto (ou não). Usando essa técnica, você pode gerar um histórico de pedidos para cada produto. Não tenho certeza se isso é útil para você neste contexto, mas achei bastante útil. Aqui está o código

OrdersController#show
  @order = Order.find(params[:id])
  @order_items_by_photo = @order.order_items.group_by(&:photo)

@order_items_by_photo então se parece com isto:

=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]

Então você pode fazer algo como:

@orders_by_product = @user.orders.group_by(&:product)

Então, quando você conseguir isso em sua visão, apenas percorra algo assim:

- for product, orders in @user.orders_by_product
  - "#{product.name}: #{orders.size}"
  - for order in orders
    - output_order_details

Desta forma, você evita o problema de devolver apenas um produto, pois sempre sabe que ele retornará um hash com um produto como chave e um array de seus pedidos.

Pode ser um exagero para o que você está tentando fazer, mas oferece algumas opções interessantes (ou seja, datas solicitadas, etc.) para trabalhar além da quantidade.

Josh Kovach
fonte
Obrigado pela resposta detalhada. Isso pode ser um pouco mais do que eu preciso, mas é interessante aprender com ele (na verdade, posso usar isso em outro lugar no aplicativo). O que você acha sobre o desempenho para as várias abordagens mencionadas nesta página?
Andy Harvey
2

No Rails 6 eu fiz isso funcionar perfeitamente:

  has_many :regions, -> { order(:name).distinct }, through: :sites

Não consegui fazer nenhuma das outras respostas funcionar.

Sean Mitchell
fonte
O mesmo nos trilhos 5.2+
Glenn