Jak wyświetlić unikalne rekordy z has_many poprzez relację?

106

Zastanawiam się, jaki jest najlepszy sposób wyświetlania unikalnych rekordów z has_many, poprzez relację w Rails3.

Mam trzy modele:

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

Powiedzmy, że chcę wymienić wszystkie produkty zamówione przez klienta na jego stronie pokazu.

Mogli wielokrotnie zamawiać niektóre produkty, więc używam counter_cache do wyświetlania w kolejności malejącej, na podstawie liczby zamówień.

Ale jeśli zamówili produkt wiele razy, muszę upewnić się, że każdy produkt jest wymieniony tylko raz.

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

działa, gdy istnieje wiele rekordów zamówień na produkt, ale generuje błąd, jeśli produkt został zamówiony tylko raz. (ranking to niestandardowa funkcja sortowania zdefiniowana w innym miejscu)

Inną alternatywą jest:

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

Nie jestem pewien, czy mam tutaj właściwe podejście.

Czy ktoś jeszcze się tym zajął? Jakie problemy napotkaliście? Gdzie mogę dowiedzieć się więcej o różnicy między .unique! i DISTINCT ()?

Jaki jest najlepszy sposób na wygenerowanie listy unikatowych rekordów za pomocą has_many, poprzez relację?

Dzięki

Andy Harvey
źródło

Odpowiedzi:

237

Czy próbowałeś określić opcję: uniq w asocjacji has_many:

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

Z dokumentacji Railsów :

:uniq

Jeśli prawda, duplikaty zostaną pominięte w kolekcji. Przydatne w połączeniu z: through.

AKTUALIZACJA DLA KOLEJÓW 4:

W Rails 4 has_many :products, :through => :orders, :uniq => truejest przestarzały. Zamiast tego powinieneś teraz napisać has_many :products, -> { distinct }, through: :orders. Aby uzyskać więcej informacji, zobacz odrębną sekcję dotyczącą relacji has_many:: through w dokumentacji ActiveRecord Associations . Dziękuję Kurtowi Muellerowi za wskazanie tego w swoim komentarzu.

mbreining
źródło
6
Różnica nie polega na tym, czy usuwasz duplikaty w modelu lub kontrolerze, ale raczej używasz opcji: uniq w swoim skojarzeniu (jak pokazano w mojej odpowiedzi) lub SQL DISTINCT stmt (np. Has_many: products,: through =>: orders ,: select => "DISTINCT products. *). W pierwszym przypadku WSZYSTKIE rekordy są pobierane, a rails usuwają za ciebie duplikaty. W późniejszym przypadku z bazy danych są pobierane tylko nie zduplikowane rekordy, więc może to zapewnić lepszą wydajność jeśli masz duży zestaw wyników
mbreining
68
W Rails 4 has_many :products, :through => :orders, :uniq => truejest przestarzały. Zamiast tego powinieneś teraz napisać has_many :products, -> { uniq }, through: :orders.
Kurt Mueller
8
Zauważ, że -> {uniq} w tym sensie jest po prostu aliasem dla -> {odrębny} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Występuje w języku SQL, a nie w ruby
engineerDave
5
Jeśli masz konflikt z klauzulami DISTINCTi ORDER BY, zawsze możesz użyćhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
dzięki @fagiani i jeśli twój model ma kolumnę json i używasz psql, to staje się jeszcze bardziej skomplikowane i musisz zrobić coś takiego, jak has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationinaczej otrzymaszrails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Zauważ, że uniq: truezostało usunięte z poprawnych opcji has_manyw Rails 4.

W Railsach 4 musisz podać zakres, aby skonfigurować tego rodzaju zachowanie. Zakresy mogą być dostarczane przez lambdy, na przykład:

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

Przewodnik po railsach omawia ten i inne sposoby wykorzystania zakresów do filtrowania zapytań relacji, przewiń w dół do sekcji 4.3.3:

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

Yoshiki
źródło
4
W Railsach 5.1 ciągle otrzymywałem undefined method 'except' for #<Array...i to dlatego, że .uniqzamiast.distinct
stevenspiel
5

Możesz użyć group_by. Na przykład mam koszyk z galerią zdjęć, dla którego chcę, aby pozycje były sortowane według tego, które zdjęcie (każde zdjęcie można zamówić wiele razy i na odbitkach w różnych rozmiarach). To następnie zwraca hash z produktem (zdjęcie) jako klucz i za każdym razem, gdy został zamówiony, można go wymienić w kontekście zdjęcia (lub nie). Korzystając z tej techniki, można faktycznie wyświetlić historię zamówień dla każdego produktu. Nie jestem pewien, czy jest to pomocne w tym kontekście, ale uznałem to za całkiem przydatne. Oto kod

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

@order_items_by_photo wtedy wygląda mniej więcej tak:

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

Możesz więc zrobić coś takiego:

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

Następnie, gdy zobaczysz to w swoim widoku, po prostu przejrzyj coś takiego:

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

W ten sposób unikniesz problemu występującego przy zwracaniu tylko jednego produktu, ponieważ zawsze wiesz, że zwróci on skrót z produktem jako kluczem i tablicą Twoich zamówień.

Może to być przesada, jeśli chodzi o to, co próbujesz zrobić, ale daje ci kilka fajnych opcji (np. Zamówione daty itp.) Do pracy oprócz ilości.

Josh Kovach
źródło
dzięki za szczegółową odpowiedź. Może to być trochę więcej, niż potrzebuję, ale mimo to interesujące do nauczenia się (właściwie mogę tego użyć gdzie indziej w aplikacji). Co sądzisz o wydajności dla różnych podejść wymienionych na tej stronie?
Andy Harvey
2

Na Rails 6 mam to, aby działało idealnie:

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

Żadne inne odpowiedzi nie zadziałały.

Sean Mitchell
źródło
To samo na szynach 5.2+
Glenn