Як відобразити унікальні записи з 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, через зв’язок?

Дякую

Відповіді:


235

Ви намагалися вказати параметр: uniq в асоціації has_many:

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

З документації Rails :

:uniq

Якщо це правда, дублікати будуть опущені з колекції. Корисно в поєднанні з: наскрізь.

ОНОВЛЕННЯ ДЛЯ РЕЙЛІВ 4:

У рейки 4 has_many :products, :through => :orders, :uniq => trueзастаріла. Натомість вам слід писати зараз has_many :products, -> { distinct }, through: :orders. Додаткову інформацію див. У розділі has_many:: через зв’язки в документації на асоціації ActiveRecord для отримання додаткової інформації. Дякуємо Курту Мюллеру за те, що він це вказав у своєму коментарі.


6
Різниця не стільки в тому, чи ви видаляєте дублікати в моделі чи контролері, а скоріше використовуєте параметр: uniq у вашій асоціації (як показано у моїй відповіді) або stmt SQL DISTINCT (наприклад, has_many: products,: through =>: order ,: select => "DISTINCT products. *). У першому випадку витягуються ВСІ записи і рейки видаляють для вас дублікати. У пізньому випадку з db витягуються лише не повторювані записи, що може забезпечити кращу продуктивність якщо у вас є великий набір результатів
mbreining

68
У рейки 4 has_many :products, :through => :orders, :uniq => trueзастаріла. Натомість вам слід писати зараз has_many :products, -> { uniq }, through: :orders.
Курт Мюллер

8
Зауважте, що -> {uniq} у цьому сенсі - це лише псевдонім для -> { distct } apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Це відбувається в SQL, а не в рубіні
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_manyRails 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+
Гленн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.