2010-02-26 8 views
1

J'ai quelques modèles dans une application Rails. Disons que c'est les produits et les ventes.Obtention d'un modèle de rails avec statistiques

Un produit a beaucoup de ventes et une vente appartient à un produit.

Ce que je cherche à faire est d'obtenir une liste de tous les produits (éventuellement avec des conditions) et à côté de chaque produit de fournir un décompte du nombre de ventes, similaires à:

  • Produit 1 (10)
  • produit 2 (22)

Alors que la sortie est assez simple, je ne peux pas trouver un moyen agréable de recueillir les données sans utiliser SQL.

Je devrais également ajouter que je peux vouloir appliquer une condition aux ventes aussi.

Répondre

1

Si vous déclarez correctement la associations, c'est aussi simple que:

Product.find(:all, :include => :sales).each do |product| 
    puts product.name + " (#{product.sales.size})" 
end 

EDIT:
Les associations sont riches collections qui peuvent être recherchées si vous avez besoin d'appliquer une condition:

@products = Product.find(:all, :include => :sales) 
@products.each do |product| 
    # Find product sales only from yesterday 
    yesterdays_sales = product.sales.find(:all, :conditions => { :created_at => Date.yesterday } 
end 
+0

Et là, vous avez le problème «N + 1». –

+0

Il n'y a aucune raison d'optimiser prématurément. Les associations sont The Rails Way et seront familiers à tous les développeurs de Rails qui regardent son code. Il peut toujours optimiser plus tard si cela devient un goulot d'étranglement. –

+0

Ce n'est pas une optimisation prématurée. Utilisez simplement ': include' ou': join'. Vous ne passez pas de temps pour ça. Pourquoi devrions-nous écrire un mauvais code à l'avance sachant que c'est mauvais si nous pouvons écrire un meilleur code avec exactement le même effort maintenant. –

0

Si vous ne me dérange pas d'apporter les objets de vente connexes de la base de données dans l'application puis quelque chose like this sho uld faire le travail:

Mais gardez un œil dessus.

# Preload all the Sales for our Products, sow we won't end up with `N+1` issue. 
selected_products = Product.all :joins => :sales, :conditions => {'orders.successful' => true} 

# Then use it like a normal association: 
selected_products.each { |p| puts p.sales.size } 

Cordialement,
Dmitriy.

+0

Votre solution a le problème N + 1. L'appel 'Product.all' utilise la table' join' dans la condition 'WHERE'. Lors de vos appels ultérieurs à p.sales, une nouvelle requête sera envoyée à la base de données pour obtenir les ventes associées au produit. J'ai donné une réponse détaillée ici: (http://stackoverflow.com/questions/2338605/getting-a-rails-model-with-statistics/2339708#2339708). –

+0

@KandadaBoggu, Merci beaucoup pour cela. Je croyais que 'sales.size' n'exécuterait pas de requête car il est supposé être chargé avec': join'. Besoin d'aller y jeter un coup d'œil. –

+0

Pas de soucis. A cause de vous, j'ai pu confirmer le soupçon que j'avais toujours eu à propos de "se joindre". –

1

Si vous êtes surtout intéressé à compter les ventes par produit, vous devriez aller avec la fonctionnalité counter_cache. Cela garantira que vous obtenez toujours le nombre de la table products au lieu des calculs de nombre JIT.

# products table should have a column called sales_count 
class Product < ActiveRecord:Base 
    has_many :sales 
end 

class Sales < ActiveRecord:Base 
    belongs_to :product, :counter_cache => true 
end 

Les rails se chargeront de incrémenter/décrémenter la colonne sales_count lors de la création/suppression des ventes. Maintenant, vous pouvez obtenir le nombre de ventes en procédant comme suit:

Product.first.sales_count 

Pour compter conditionnellement les sales sur un seul objet product procédez comme suit:

Product.first.sales.count(:conditions => ["amount > ? ", 200]) 

Pour compter conditionnellement un lot de product s faire ce qui suit:

#returns a ordered hash 
product_sales_count = Product.count(:joins => [:sales], :conditions => 
      ["amount > ? ", 200], :group =>"products.id").each do |product_id, sales_count| 
    p "Product #{product_id} = #{sales_count}" 
end 

# If you need the product objects get all of them in one call and match the count 
products = Product.find(:all, :joins => [:sales], :conditions => 
      ["amount > ? ", 200], :group =>"products.id").each do |product| 

    p "Product #{product.name} = #{product_sales_count[product.id]}" 

end 

Cette approche permettra d'économiser beaucoup de voyages à la base de données.

+0

'Product.first.sales.count (: conditions => [" montant>? ", 200])' exécutera une requête par produit. Alors, quel est le point d'avoir: counter_cache s'il y a un besoin de comptes conditionnels? –

+0

Il finit avec 'N + 1'. –

+0

Comme je l'ai mentionné dans ma réponse, il devrait choisir counter_cache UNIQUEMENT s'il compte la plupart du temps les ventes sans conditions. J'ai modifié ma réponse pour résoudre le problème N + 1 du scénario conditionnel. Encore une fois cela dépend de son cas d'utilisation. –

2

Ceci n'est pas une réponse mais la comparaison de trois réponses à cette question. J'ai déjà donné ma réponse. Il y a confusion avec le fonctionnement de :joins et :include dans ActiveRecord.find.J'ai donc passé du temps à analyser le journal SQL pour trouver trois solutions.

Approche 1: obtenir le nombre de ventes en utilisant count

#To return the sales count for each product 
    ps_count_hash = Product.count(:joins => [:sales], :group => "products.id") # sql 1.1 

    # To print the product id and sales count 
    ps_count_hash.each do | product_id, sales_count| 
    p "Product#{product_id} - (#{sales_count})" 
    end 

    # To print the product details and sales count 
    # get all the products associated 
    Product.find_all_by_id(ps_count_hash.keys).each |product| #sql 1.2 
    p "Product[id = #{product.id}, name = #{product.name}] - (#{ps_count_hash[product.id]})" 
    end 

Approche 2: Obtenez les produits par un join

Product.find(:all, :joins=>[:sales]).each do |product| #sql 2.1 
    p "Product[id = #{product.id}, name = #{product.name}] - (#{product.sales.size})" # sql 2.2 - 2.(2+N) 
    end 

Approche 3: Obtenez les produits à travers un include

Product.find(:all, :include=>[:sales]).each do |product| #sql 3.1 and 3.2 
    p "Product[id = #{product.id}, name = #{product.name}] - (#{product.sales.size})" 
    end 

Maintenant, regardons les instructions SQL générées par ces trois approches

instructions SQL pour l'approche 1 - 2 SQL

SELECT count(*) AS count_all, products.id AS products_id FROM `products` INNER JOIN `sales` ON sales.product_id = products.id GROUP BY products.id 
SELECT `products`.* FROM `products` WHERE (`products`.`id` IN (1,2)) 

instructions SQL pour l'approche 2 - 2 SQL

SELECT * FROM `products` 
SELECT `sales`.* FROM `sales` WHERE (`sales`.product_id IN (1,2)) 

Instructions SQL pour l'approche 3 - N + 1 SQL

SELECT `products`.* FROM `products` INNER JOIN `sales` ON sales.product_id = products.id 
SELECT * FROM `sales` WHERE (`sales`.product_id = 1) 
SELECT * FROM `sales` WHERE (`sales`.product_id = 2) 

Meilleure approche pour le comptage produit par les ventes (avec des détails sur les ventes): Approche 1

Meilleure approche pour le comptage produit par les ventes (avec des ventes de détails): Approche 2

Approche 3 a le problème N + 1. Donc, c'est hors de la contestation.

Questions connexes