Come visualizzare record univoci da una relazione has_many through?


106

Mi chiedo quale sia il modo migliore per visualizzare record univoci da has_many, attraverso la relazione in Rails3.

Ho tre modelli:

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

Diciamo che voglio elencare tutti i prodotti che un cliente ha ordinato nella sua pagina di presentazione.

Potrebbero aver ordinato alcuni prodotti più volte, quindi sto usando counter_cache per visualizzare in ordine di classificazione decrescente, in base al numero di ordini.

Tuttavia, se hanno ordinato un prodotto più volte, devo assicurarmi che ogni prodotto sia elencato una sola volta.

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

funziona quando sono presenti più record di ordine per un prodotto, ma genera un errore se un prodotto è stato ordinato una sola volta. (classificato è una funzione di ordinamento personalizzata definita altrove)

Un'altra alternativa è:

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

Non sono sicuro di essere sull'approccio giusto qui.

Qualcun altro ha affrontato questo? Quali problemi hai incontrato? Dove posso trovare maggiori informazioni sulla differenza tra .unique! e DISTINCT ()?

Qual è il modo migliore per generare un elenco di record univoci tramite has_many, tramite relazione?

Grazie

Risposte:


235

Hai provato a specificare l'opzione: uniq nell'associazione has_many:

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

Dalla documentazione di Rails :

:uniq

Se true, i duplicati verranno omessi dalla raccolta. Utile in combinazione con: through.

AGGIORNAMENTO PER RAILS 4:

In Rails 4, has_many :products, :through => :orders, :uniq => trueè deprecato. Invece, ora dovresti scrivere has_many :products, -> { distinct }, through: :orders. Vedere la sezione distinta per has_many:: attraverso le relazioni sulla documentazione di ActiveRecord Associations per ulteriori informazioni. Grazie a Kurt Mueller per averlo sottolineato nel suo commento.


6
La differenza non è tanto se rimuovi i duplicati nel modello o nel controller, ma piuttosto usi l'opzione: uniq sulla tua associazione (come mostrato nella mia risposta) o SQL DISTINCT stmt (ad esempio has_many: products,: through =>: orders ,: select => "DISTINCT products. *). Nel primo caso, vengono recuperati TUTTI i record e rails rimuove i duplicati per te. Nel caso successivo, solo i record non duplicati vengono recuperati dal db in modo che possa offrire prestazioni migliori se hai un set di risultati di grandi dimensioni.
mbreining

68
In Rails 4, has_many :products, :through => :orders, :uniq => trueè deprecato. Invece, ora dovresti scrivere has_many :products, -> { uniq }, through: :orders.
Kurt Mueller

8
Si noti che -> {uniq} in questo senso è solo un alias per -> {separate} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Si verifica in SQL non in ruby
engineerDave

5
In caso di conflitto con clausole DISTINCTe ORDER BY, puoi sempre utilizzarehas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani

1
grazie @fagiani e se il tuo modello ha una colonna json e usi psql diventa ancora più complicato e devi fare qualcosa del genere has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationaltrimenti ottienirails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9

43

Nota che uniq: trueè stato rimosso dalle opzioni valide per has_manyRails 4.

In Rails 4 devi fornire uno scope per configurare questo tipo di comportamento. Gli ambiti possono essere forniti tramite lambda, in questo modo:

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

La guida ai binari copre questo e altri modi in cui puoi usare gli ambiti per filtrare le query della tua relazione, scorri verso il basso fino alla sezione 4.3.3:

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


4
In Rails 5.1, ho continuato a ricevere undefined method 'except' for #<Array...ed è stato perché ho usato .uniqinvece di.distinct
stevenspiel

5

Potresti usare group_by. Ad esempio, ho un carrello della galleria fotografica per il quale desidero ordinare gli articoli in base a quale foto (ogni foto può essere ordinata più volte e in stampe di dimensioni diverse). Questo quindi restituisce un hash con il prodotto (foto) come chiave e ogni volta che è stato ordinato può essere elencato nel contesto della foto (o meno). Usando questa tecnica, potresti effettivamente produrre una cronologia degli ordini per ogni dato prodotto. Non sono sicuro che ti sia utile in questo contesto, ma l'ho trovato abbastanza utile. Ecco il codice

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

@order_items_by_photo quindi assomiglia a questo:

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

Quindi potresti fare qualcosa come:

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

Quindi, quando ottieni questo nella tua vista, fai semplicemente scorrere qualcosa del genere:

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

In questo modo eviti il ​​problema riscontrato quando restituisci un solo prodotto, poiché sai sempre che restituirà un hash con un prodotto come chiave e un array dei tuoi ordini.

Potrebbe essere eccessivo per quello che stai cercando di fare, ma ti offre alcune belle opzioni (ad esempio date ordinate, ecc.) Con cui lavorare oltre alla quantità.


grazie per la risposta dettagliata. Questo potrebbe essere un po 'più del necessario, ma comunque interessante da cui imparare (in realtà potrei usarlo da qualche altra parte nell'app). Quali sono i tuoi pensieri sulla performance per i vari approcci menzionati in questa pagina?
Andy Harvey

2

Su Rails 6 ho fatto funzionare perfettamente questo:

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

Non sono riuscito a far funzionare nessuna delle altre risposte.


Lo stesso su rails 5.2+
Glenn
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.