¿Cómo mostrar registros únicos de has_many a través de la relación?

106

Me pregunto cuál es la mejor manera de mostrar registros únicos de has_many, a través de la relación en Rails3.

Tengo tres 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 quiero enumerar todos los productos que un cliente ha pedido en su página de presentación.

Es posible que hayan pedido algunos productos varias veces, por lo que estoy usando counter_cache para mostrarlos en orden de clasificación descendente, según la cantidad de pedidos.

Pero, si han pedido un producto varias veces, debo asegurarme de que cada producto solo aparezca una vez.

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

funciona cuando hay varios registros de pedidos para un producto, pero genera un error si un producto solo se ha pedido una vez. (clasificado es la función de clasificación personalizada definida en otro lugar)

Otra alternativa es:

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

No estoy seguro de estar en el enfoque correcto aquí.

¿Alguien más ha abordado esto? ¿A qué problemas te enfrentaste? ¿Dónde puedo encontrar más información sobre la diferencia entre .unique! y DISTINCT ()?

¿Cuál es la mejor manera de generar una lista de registros únicos a través de una relación has_many?

Gracias

Andy Harvey
fuente

Respuestas:

235

¿Ha intentado especificar la opción: uniq en la asociación has_many?

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

De la documentación de Rails :

:uniq

Si es verdadero, los duplicados se omitirán de la colección. Útil junto con: through.

ACTUALIZACIÓN PARA RAILS 4:

En Rails 4, has_many :products, :through => :orders, :uniq => trueestá en desuso. En su lugar, ahora debería escribir has_many :products, -> { distinct }, through: :orders. Consulte la sección distinta para has_many:: a través de relaciones en la documentación de Asociaciones de ActiveRecord para obtener más información. Gracias a Kurt Mueller por señalar esto en su comentario.

mbreining
fuente
6
La diferencia no es tanto si elimina los duplicados en el modelo o el controlador, sino que usa la opción: uniq en su asociación (como se muestra en mi respuesta) o el stmt SQL DISTINCT (por ejemplo, has_many: productos,: a través de =>: pedidos ,: select => "productos DISTINCT. *). En el primer caso, se obtienen TODOS los registros y los rieles eliminan los duplicados. En el último caso, solo se obtienen de la base de datos los registros no duplicados, por lo que podría ofrecer un mejor rendimiento si tiene un conjunto de resultados grande
mbreining
68
En Rails 4, has_many :products, :through => :orders, :uniq => trueestá en desuso. En su lugar, ahora debería escribir has_many :products, -> { uniq }, through: :orders.
Kurt Mueller
8
Tenga en cuenta que -> {uniq} en este sentido es solo un alias para -> {distinto} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Ocurre en SQL, no en ruby
EngineerDave
5
Si tiene un conflicto con las cláusulas DISTINCTy ORDER BY, siempre puede usarhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
gracias @fagiani y si su modelo tiene una columna json y usa psql, se vuelve más complicado aún y tiene que hacer algo como, de lo has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationcontrario, obtienerails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Tenga en cuenta que uniq: truese ha eliminado de las opciones válidas para has_manyRails 4.

En Rails 4 tienes que proporcionar un alcance para configurar este tipo de comportamiento. Los alcances se pueden suministrar a través de lambdas, así:

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

La guía de rieles cubre esta y otras formas en las que puede usar ámbitos para filtrar las consultas de su relación, desplácese hacia abajo hasta la sección 4.3.3:

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

Yoshiki
fuente
4
En Rails 5.1, seguí recibiendo undefined method 'except' for #<Array...y fue porque usé en .uniqlugar de.distinct
stevenspiel
5

Podrías usar group_by. Por ejemplo, tengo un carrito de compras de la galería de fotos para el cual quiero ordenar los artículos por qué foto (cada foto se puede pedir varias veces y en impresiones de diferentes tamaños). Esto luego devuelve un hash con el producto (foto) como clave y cada vez que se ordenó se puede enumerar en el contexto de la foto (o no). Con esta técnica, podría generar un historial de pedidos para cada producto dado. No estoy seguro de si eso es útil para usted en este contexto, pero lo encontré bastante útil. Aqui esta el codigo

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

@order_items_by_photo luego se ve algo como esto:

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

Entonces podrías hacer algo como:

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

Luego, cuando tenga esto en su vista, simplemente recorra algo como esto:

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

De esta manera evita el problema que se ve al devolver un solo producto, ya que siempre sabe que devolverá un hash con un producto como clave y una matriz de sus pedidos.

Puede ser exagerado para lo que está tratando de hacer, pero le brinda algunas opciones agradables (es decir, fechas ordenadas, etc.) con las que trabajar además de la cantidad.

Josh Kovach
fuente
gracias por la respuesta detallada. Esto puede ser un poco más de lo que necesito, pero de todos modos es interesante aprender de él (de hecho, puedo usar esto en otro lugar de la aplicación). ¿Qué piensa sobre el rendimiento de los distintos enfoques mencionados en esta página?
Andy Harvey
2

En Rails 6 conseguí que esto funcionara perfectamente:

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

No pude hacer que ninguna de las otras respuestas funcionara.

Sean Mitchell
fuente
Lo mismo en rieles 5.2+
Glenn