Wie werden eindeutige Datensätze aus einer has_many-Beziehung angezeigt?

106

Ich frage mich, wie ich eindeutige Datensätze aus has_many über eine Beziehung in Rails3 am besten anzeigen kann.

Ich habe drei Modelle:

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

Nehmen wir an, ich möchte alle Produkte, die ein Kunde bestellt hat, auf seiner Ausstellungsseite auflisten.

Möglicherweise haben sie einige Produkte mehrmals bestellt, daher verwende ich counter_cache, um sie in absteigender Reihenfolge anzuzeigen, basierend auf der Anzahl der Bestellungen.

Wenn sie jedoch ein Produkt mehrmals bestellt haben, muss ich sicherstellen, dass jedes Produkt nur einmal aufgeführt wird.

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

Funktioniert, wenn für ein Produkt mehrere Bestelldatensätze vorhanden sind, generiert jedoch einen Fehler, wenn ein Produkt nur einmal bestellt wurde. (Rang ist die an anderer Stelle definierte benutzerdefinierte Sortierfunktion)

Eine andere Alternative ist:

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

Ich bin nicht sicher, ob ich hier auf dem richtigen Weg bin.

Hat jemand anderes das angegangen? Auf welche Probleme sind Sie gestoßen? Wo kann ich mehr über den Unterschied zwischen .unique erfahren? und DISTINCT ()?

Was ist der beste Weg, um eine Liste eindeutiger Datensätze über eine has_many-Beziehung zu generieren?

Vielen Dank

Andy Harvey
quelle

Antworten:

235

Haben Sie versucht, die Option: uniq in der Zuordnung has_many anzugeben:

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

Aus der Rails-Dokumentation :

:uniq

Wenn true, werden Duplikate aus der Sammlung weggelassen. Nützlich in Verbindung mit: durch.

UPDATE FÜR SCHIENEN 4:

In Rails 4 has_many :products, :through => :orders, :uniq => trueist veraltet. Stattdessen sollten Sie jetzt schreiben has_many :products, -> { distinct }, through: :orders. Weitere Informationen finden Sie im entsprechenden Abschnitt für has_many :: through-Beziehungen in der Dokumentation zu ActiveRecord Associations . Vielen Dank an Kurt Mueller für den Hinweis in seinem Kommentar.

mbreining
quelle
6
Der Unterschied besteht nicht so sehr darin, ob Sie Duplikate im Modell oder im Controller entfernen, sondern die Option: uniq für Ihre Zuordnung (wie in meiner Antwort gezeigt) oder den SQL DISTINCT-Befehl (z. B. has_many: products ,: through =>: orders) verwenden ,: select => "DISTINCT-Produkte. *). Im ersten Fall werden ALLE Datensätze abgerufen und Rails entfernt die Duplikate für Sie. Im späteren Fall werden nur nicht doppelte Datensätze aus der Datenbank abgerufen, um möglicherweise eine bessere Leistung zu erzielen wenn Sie eine große Ergebnismenge haben.
mbreining
68
In Rails 4 has_many :products, :through => :orders, :uniq => trueist veraltet. Stattdessen sollten Sie jetzt schreiben has_many :products, -> { uniq }, through: :orders.
Kurt Mueller
8
Beachten Sie, dass -> {uniq} in diesem Sinne nur ein Alias ​​für -> {different} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq ist. Es tritt in SQL nicht ruby
engineeringDave
5
Wenn Sie einen Konflikt mit DISTINCTund ORDER BYKlauseln bekommen, können Sie immer verwendenhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
danke @fagiani und wenn dein Modell eine json-Spalte hat und du psql verwendest, wird es noch komplizierter und du musst etwas tun, wie has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationdu es sonst bekommstrails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Beachten Sie, dass uniq: truedies ab has_manyRails 4 aus den gültigen Optionen entfernt wurde .

In Rails 4 müssen Sie einen Bereich angeben, um diese Art von Verhalten zu konfigurieren. Zielfernrohre können wie folgt über Lambdas geliefert werden:

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

Die Rails-Anleitung behandelt diese und andere Möglichkeiten, wie Sie Bereiche verwenden können, um die Abfragen Ihrer Beziehung zu filtern. Scrollen Sie nach unten zu Abschnitt 4.3.3:

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

Yoshiki
quelle
4
In Rails 5.1 bekam ich immer undefined method 'except' for #<Array...und es war, weil ich .uniqanstelle von.distinct
Stevenspiel
5

Sie könnten verwenden group_by. Zum Beispiel habe ich einen Fotogalerie-Einkaufswagen, für den ich möchte, dass Bestellartikel nach welchem ​​Foto sortiert werden (jedes Foto kann mehrmals und in verschiedenen Druckgrößen bestellt werden). Dies gibt dann einen Hash mit dem Produkt (Foto) als Schlüssel zurück und jedes Mal, wenn es bestellt wurde, kann es im Kontext des Fotos aufgelistet werden (oder nicht). Mit dieser Technik können Sie tatsächlich eine Bestellhistorie für jedes Produkt ausgeben. Ich bin mir nicht sicher, ob das für Sie in diesem Zusammenhang hilfreich ist, aber ich fand es sehr nützlich. Hier ist der Code

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

@order_items_by_photo dann sieht ungefähr so ​​aus:

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

Sie könnten also so etwas tun:

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

Wenn Sie dies in Ihrer Ansicht sehen, durchlaufen Sie einfach Folgendes:

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

Auf diese Weise vermeiden Sie das Problem, das bei der Rücksendung nur eines Produkts auftritt, da Sie immer wissen, dass ein Hash mit einem Produkt als Schlüssel und einer Reihe Ihrer Bestellungen zurückgegeben wird.

Es ist vielleicht übertrieben für das, was Sie versuchen, aber es gibt Ihnen einige nette Optionen (z. B. bestellte Daten usw.), mit denen Sie zusätzlich zur Menge arbeiten können.

Josh Kovach
quelle
danke für die ausführliche antwort. Dies ist vielleicht etwas mehr als ich brauche, aber es ist trotzdem interessant, daraus zu lernen (tatsächlich kann ich dies irgendwo anders in der App verwenden). Was denken Sie über die Leistung der verschiedenen auf dieser Seite genannten Ansätze?
Andy Harvey
2

Auf Rails 6 habe ich das perfekt zum Laufen gebracht:

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

Ich konnte keine der anderen Antworten zur Arbeit bringen.

Sean Mitchell
quelle
Gleiches gilt für Rails 5.2+
Glenn