Comment afficher les enregistrements uniques d'une relation has_many through?

106

Je me demande quelle est la meilleure façon d'afficher des enregistrements uniques à partir d'un has_many, via une relation dans Rails3.

J'ai trois modèles:

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

Disons que je veux lister tous les produits qu'un client a commandés sur sa page de présentation.

Ils ont peut-être commandé certains produits plusieurs fois, j'utilise donc counter_cache pour les afficher par ordre décroissant, en fonction du nombre de commandes.

Mais, s'ils ont commandé un produit plusieurs fois, je dois m'assurer que chaque produit n'est répertorié qu'une seule fois.

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

fonctionne lorsqu'il existe plusieurs enregistrements de commande pour un produit, mais génère une erreur si un produit n'a été commandé qu'une seule fois. (le classement est une fonction de tri personnalisée définie ailleurs)

Une autre alternative est:

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

Je ne suis pas convaincu que je suis sur la bonne approche ici.

Quelqu'un d'autre s'est-il attaqué à cela? Quels problèmes avez-vous rencontrés? Où puis-je en savoir plus sur la différence entre .unique! et DISTINCT ()?

Quelle est la meilleure façon de générer une liste d'enregistrements uniques via une relation has_many, through?

Merci

Andy Harvey
la source

Réponses:

237

Avez-vous essayé de spécifier l'option: uniq sur l'association has_many:

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

À partir de la documentation Rails :

:uniq

Si vrai, les doublons seront omis de la collection. Utile en conjonction avec: à travers.

MISE À JOUR POUR RAILS 4:

Dans Rails 4, has_many :products, :through => :orders, :uniq => trueest obsolète. Au lieu de cela, vous devriez maintenant écrire has_many :products, -> { distinct }, through: :orders. Consultez la section distincte pour has_many:: through relations dans la documentation des associations ActiveRecord pour plus d'informations. Merci à Kurt Mueller de l'avoir souligné dans son commentaire.

métissage
la source
6
La différence n'est pas tant que vous supprimiez les doublons dans le modèle ou le contrôleur, mais plutôt que vous utilisiez l'option: uniq sur votre association (comme indiqué dans ma réponse) ou le SQL DISTINCT stmt (par exemple has_many: products,: through =>: orders ,: select => "DISTINCT products. *). Dans le premier cas, TOUS les enregistrements sont récupérés et les rails suppriment les doublons pour vous. Dans le dernier cas, seuls les enregistrements non dupliqués sont extraits de la base de données afin d'offrir de meilleures performances si vous avez un grand ensemble de résultats.
mbreining
68
Dans Rails 4, has_many :products, :through => :orders, :uniq => trueest obsolète. Au lieu de cela, vous devriez maintenant écrire has_many :products, -> { uniq }, through: :orders.
Kurt Mueller
8
Notez que -> {uniq} dans ce sens est juste un alias pour -> {distinct} apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq Cela se produit en SQL et non en ruby
engineerDave
5
Si vous obtenez un conflit avec les clauses DISTINCTet ORDER BY, vous pouvez toujours utiliserhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani
1
merci @fagiani et si votre modèle a une colonne json et que vous utilisez psql, cela devient encore plus compliqué et vous devez faire quelque chose comme has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationsinon vous obtenezrails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9
43

Notez que cela uniq: truea été supprimé des options valides pour à has_manypartir de Rails 4.

Dans Rails 4, vous devez fournir une portée pour configurer ce type de comportement. Les portées peuvent être fournies via lambdas, comme ceci:

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

Le guide des rails couvre ceci et d'autres façons d'utiliser les étendues pour filtrer les requêtes de votre relation, faites défiler jusqu'à la section 4.3.3:

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

Yoshiki
la source
4
Dans Rails 5.1, j'ai continué à obtenir undefined method 'except' for #<Array...et c'était parce que j'ai utilisé à la .uniqplace de.distinct
stevenspiel
5

Vous pourriez utiliser group_by. Par exemple, j'ai un panier de galerie de photos pour lequel je souhaite que les articles de commande soient triés par photo (chaque photo peut être commandée plusieurs fois et dans des impressions de tailles différentes). Cela renvoie ensuite un hachage avec le produit (photo) comme clé et chaque fois qu'il a été commandé peut être répertorié dans le contexte de la photo (ou non). En utilisant cette technique, vous pouvez en fait générer un historique des commandes pour chaque produit donné. Je ne sais pas si cela vous est utile dans ce contexte, mais je l'ai trouvé très utile. Voici le code

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

@order_items_by_photo alors ressemble à quelque chose comme ceci:

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

Vous pouvez donc faire quelque chose comme:

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

Ensuite, lorsque vous obtenez ceci dans votre vue, parcourez simplement quelque chose comme ceci:

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

De cette façon, vous évitez le problème rencontré lors du retour d'un seul produit, car vous savez toujours qu'il renverra un hachage avec un produit comme clé et un tableau de vos commandes.

Cela peut être exagéré pour ce que vous essayez de faire, mais cela vous donne de belles options (par exemple, les dates commandées, etc.) avec lesquelles travailler en plus de la quantité.

Josh Kovach
la source
Merci pour la réponse détaillée. C'est peut-être un peu plus que ce dont j'ai besoin, mais il est néanmoins intéressant d'en tirer des leçons (en fait, je peux l'utiliser ailleurs dans l'application). Que pensez-vous de la performance pour les différentes approches mentionnées sur cette page?
Andy Harvey
2

Sur Rails 6, cela fonctionne parfaitement:

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

Je n'ai pas réussi à faire fonctionner les autres réponses.

Sean Mitchell
la source
Idem sur les rails 5.2+
Glenn le