Как отобразить уникальные записи из has_many через отношения?

106

Мне интересно, как лучше всего отображать уникальные записи из has_many через отношения в Rails3.

У меня три модели:

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

Допустим, я хочу перечислить все продукты, заказанные клиентом, на их странице выставки.

Возможно, они заказывали некоторые продукты несколько раз, поэтому я использую counter_cache для отображения в порядке убывания ранжирования в зависимости от количества заказов.

Но если они заказывали продукт несколько раз, мне нужно убедиться, что каждый продукт указан только один раз.

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

работает, когда для продукта имеется несколько записей о заказах, но генерирует ошибку, если продукт был заказан только один раз. (ранжированный - это настраиваемая функция сортировки, определенная в другом месте)

Другая альтернатива:

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

Я не уверен, что придерживаюсь правильного подхода.

Кто-нибудь еще занимался этим? С какими проблемами вы столкнулись? Где я могу узнать больше о разнице между .unique! и DISTINCT ()?

Как лучше всего создать список уникальных записей через has_many, через отношения?

Спасибо

Энди Харви
источник

Ответы:

237

Вы пытались указать параметр: uniq в ассоциации has_many:

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

Из документации Rails :

:uniq

Если это правда, дубликаты будут исключены из коллекции. Полезно вместе с: through.

ОБНОВЛЕНИЕ ДЛЯ RAILS 4:

В Rails 4 has_many :products, :through => :orders, :uniq => trueне рекомендуется. Вместо этого вы должны сейчас написать has_many :products, -> { distinct }, through: :orders. См. Отдельный раздел для has_many:: через отношения в документации по ассоциациям ActiveRecord для получения дополнительной информации. Спасибо Курту Мюллеру за указание на это в своем комментарии.

mbreining
источник
6
Разница не столько в том, удаляете ли вы дубликаты в модели или контроллере, сколько в том, что вы используете опцию: uniq в своей ассоциации (как показано в моем ответе) или SQL DISTINCT stmt (например, has_many: products,: through =>: orders ,: select => "DISTINCT products. *). В первом случае извлекаются ВСЕ записи, а rails удаляет дубликаты за вас. В последнем случае из базы данных извлекаются только не повторяющиеся записи, поэтому это может обеспечить лучшую производительность если у вас большой набор результатов.
mbreining
68
В Rails 4 has_many :products, :through => :orders, :uniq => trueне рекомендуется. Вместо этого вы должны сейчас написать has_many :products, -> { uniq }, through: :orders.
Курт Мюллер
8
Обратите внимание, что -> {uniq} в этом смысле является просто псевдонимом для -> { independent } apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Это происходит в SQL, а не в ruby
EngineerDave
5
Если вы получили конфликт с DISTINCTи ORDER BYположения, вы всегда можете использоватьhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
спасибо @fagiani, и если в вашей модели есть столбец json, и вы используете psql, это становится еще сложнее, и вам нужно сделать что-то вроде, has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationиначе вы получитеrails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Обратите внимание, что uniq: trueбыло удалено из допустимых вариантов дляhas_many Rails 4.

В Rails 4 вы должны предоставить область видимости для настройки такого поведения. Области видимости могут быть предоставлены через лямбды, например:

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

В руководстве по рельсам рассказывается об этом и других способах использования областей видимости для фильтрации запросов ваших отношений, прокрутите вниз до раздела 4.3.3:

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

Йошики
источник
4
В Rails 5.1 я продолжал получать, undefined method 'except' for #<Array...и это было потому, что я использовал .uniqвместо.distinct
stevenspiel
5

Вы могли бы использовать group_by . Например, у меня есть корзина для покупок в фотогалерее, для которой я хочу, чтобы элементы заказа были отсортированы по фотографиям (каждую фотографию можно заказывать несколько раз и в распечатках разного размера). Затем он возвращает хеш с продуктом (фото) в качестве ключа, и каждый раз, когда он был заказан, он может быть указан в контексте фотографии (или нет). Используя этот метод, вы можете вывести историю заказов для каждого продукта. Не уверен, что это полезно для вас в данном контексте, но я нашел его весьма полезным. Вот код

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

@order_items_by_photo то выглядит примерно так:

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

Итак, вы могли бы сделать что-то вроде:

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

Затем, когда вы получите это в своем представлении, просто прокрутите что-то вроде этого:

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

Таким образом вы избежите проблемы, возникающей при возврате только одного продукта, поскольку вы всегда знаете, что он вернет хеш с продуктом в качестве ключа и массивом ваших заказов.

Это может быть излишним для того, что вы пытаетесь сделать, но это дает вам несколько хороших вариантов (например, заказанные даты и т. Д.), С которыми можно работать в дополнение к количеству.

Джош Ковач
источник
спасибо за подробный ответ. Это может быть немного больше, чем мне нужно, но тем не менее интересно научиться (на самом деле я могу использовать это где-нибудь еще в приложении). Что вы думаете о производительности для различных подходов, упомянутых на этой странице?
Энди Харви
2

В Rails 6 у меня получилось отлично работать:

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

Я не мог заставить работать другие ответы.

Шон Митчелл
источник
То же на рельсах 5.2+
Гленн