How to display unique records from a has_many through relationship?

Ruby on-RailsUniqueDistinctRuby on-Rails-3

Ruby on-Rails Problem Overview


I'm wondering what is the best way to display unique records from a has_many, through relationship in Rails3.

I have three models:

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

Lets say I want to list all the products a customer has ordered on their show page.

They may have ordered some products multiple times, so I'm using counter_cache to display in descending rank order, based on the number of orders.

But, if they have ordered a product multiple times, I need to ensure that each product is only listed once.

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

works when there are multiple order records for a product, but generates an error if a product has only been ordered once. (ranked is custom sort function defined elsewhere)

Another alternative is:

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

I'm not confident that I'm on the right approach here.

Has anyone else tackled this? What issues did you come up against? Where can I find out more about the difference between .unique! and DISTINCT()?

What is the best way to generate a list of unique records through a has_many, through relationship?

Thanks

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Have you tried to specify the :uniq option on the has_many association:

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

From the Rails documentation:

> :uniq > > If true, duplicates will be omitted from the collection. Useful in conjunction with :through.

UPDATE FOR RAILS 4:

In Rails 4, has_many :products, :through => :orders, :uniq => true is deprecated. Instead, you should now write has_many :products, -> { distinct }, through: :orders. See the distinct section for has_many: :through relationships on the ActiveRecord Associations documentation for more information. Thanks to Kurt Mueller for pointing this out in his comment.

Solution 2 - Ruby on-Rails

Note that uniq: true has been removed from the valid options for has_many as of Rails 4.

In Rails 4 you have to supply a scope to configure this kind of behavior. Scopes can be supplied through lambdas, like so:

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

The rails guide covers this and other ways you can use scopes to filter your relation's queries, scroll down to section 4.3.3:

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

Solution 3 - Ruby on-Rails

On Rails 6 I got this to work perfectly:

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

I couldn't get any of the other answers to work.

Solution 4 - Ruby on-Rails

You could use group_by. For example, I have a photo gallery shopping cart for which I want order items to be sorted by which photo (each photo can be ordered multiple times and in different size prints). This then returns a hash with the product (photo) as the key and each time it was ordered can be listed in context of the photo (or not). Using this technique, you could actually output an order history for each given product. Not sure if that's helpful to you in this context, but I found it quite useful. Here's the code

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

@order_items_by_photo then looks something like this:

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

So you could do something like:

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

Then when you get this in your view, just loop through something like this:

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

This way you avoid the issue seen when returning only one product, since you always know that it will return a hash with a product as the key and an array of your orders.

It might be overkill for what you're trying to do, but it does give you some nice options (i.e. dates ordered, etc.) to work with in addition to the quantity.

Solution 5 - Ruby on-Rails

in rails6 use -> { distinct } in scope it will work

class Person
  has_many :readings
  has_many :articles, -> { distinct }, through: :readings
end
 
person = Person.create(name: 'Honda')
article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect     # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAndy HarveyView Question on Stackoverflow
Solution 1 - Ruby on-RailsmbreiningView Answer on Stackoverflow
Solution 2 - Ruby on-RailsYoshikiView Answer on Stackoverflow
Solution 3 - Ruby on-RailsSean MitchellView Answer on Stackoverflow
Solution 4 - Ruby on-RailsJosh KovachView Answer on Stackoverflow
Solution 5 - Ruby on-Rails정우병View Answer on Stackoverflow