2010-06-02 12 views
2

PROBLÈMERails ActiveRecord Code amical d'un complexe Join, Somme et Regrouper

Bonjour,

J'ai pas de chance d'essayer de briser cette instruction SQL dans ActiveRecord/Rails de code amical et je J'aimerais apprendre comment je peux éviter une instruction find_by_sql dans cette situation.

Scénario J'ai des utilisateurs qui créent des audits lorsqu'ils effectuent une action. Chaque audit a une spécificité audit_activity. Chaque audit_activity vaut un certain nombre de points, basés sur score_weight. J'ai besoin de trouver les scores totaux de chaque utilisateur, en fonction de leur total cumulé audit_activity score_weights. Finalement, je vais devoir les classer, ce qui signifie ajouter une sorte à cela aussi bien.

Mon code Voici ma version sql et simplifiée des tableaux en question. Des pensées?

SQL avec les noms de colonnes complètes (pour plus de clarté)

SELECT users.id, u.email, SUM(audit_activity.score_weight) 
FROM users 
JOIN audits ON users.id = audits.user_id 
JOIN audit_activities ON audit_activities.id = audits.audit_activity_id 
GROUP BY users.id; 

Modèles: utilisateur, vérification, AuditActivity

Les champs de l'utilisateur: id, email

class User < ActiveRecord::Base 
include Clearance::User 
has_many :audits 
end 

champs d'audit: id, user_id , audit_activity_id

class Audit < ActiveRecord::Base 
    belongs_to :user 
    belongs_to :audit_activity 
end 

champs AuditActivity: id, score_weight

class AuditActivity < ActiveRecord::Base 
    has_many :audits 
end 

Exemple de données

Voici un ensemble d'instructions SQL afin que vous puissiez jouer avec des données similaires, je travaille avec et voir ce qui arrive quand l'intéressé la requête est exécutée. Vous devriez juste pouvoir copier/coller le tout dans un navigateur de requête de base de données.

CREATE TABLE users(
id INTEGER NOT NULL, 
email TEXT (25), 
PRIMARY KEY (id) 
); 

CREATE TABLE audits(
id INTEGER NOT NULL, 
user_id INTEGER, 
audit_activity_id INTEGER, 
PRIMARY KEY (id) 
); 

CREATE TABLE audit_activities(
id INTEGER NOT NULL, 
score_weight INTEGER, 
PRIMARY KEY (id) 
); 

INSERT INTO users(id, email) 
VALUES(1, "[email protected]"); 
INSERT INTO users(id, email) 
VALUES(2, "[email protected]"); 
INSERT INTO users(id, email) 
VALUES(3, "[email protected]"); 

INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(1, 1, 1); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(2, 1, 2); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(3, 1, 1); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(4, 1, 3); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(5, 1, 1); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(6, 1, 4); 

INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(7, 2, 4); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(8, 2, 4); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(9, 2, 4); 

INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(10, 3, 3); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(11, 3, 2); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(12, 3, 2); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(13, 3, 2); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(14, 3, 3); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(15, 3, 1); 
INSERT INTO audits(id, user_id, audit_activity_id) 
VALUES(16, 3, 1); 

INSERT INTO audit_activities(id, score_weight) 
VALUES(1, 1); 
INSERT INTO audit_activities(id, score_weight) 
VALUES(2, 2); 
INSERT INTO audit_activities(id, score_weight) 
VALUES(3, 7); 
INSERT INTO audit_activities(id, score_weight) 
VALUES(4, 11); 

La requête Encore une fois, est la requête ici.

SELECT u.id, u.email, SUM(aa.score_weight) 
FROM users u 
JOIN audits a ON u.id = a.user_id 
JOIN audit_activities aa ON aa.id = a.audit_activity_id 
GROUP BY u.id; 

Répondre

5
User.sum(:score_weight, :include => {:audits => :audit_activity}, :group => 'users.id') 
+0

Ceci est très proche à ce que j'ai jusqu'ici. Voyant que vous êtes arrivé avec quelque chose de très similaire, je commence à pencher plus vers cela. La vôtre, cependant, est beaucoup plus propre. Ce que j'avais: User.calculate (: sum, 'audit_activities.score_weight',: jointures => ['Audits INNER JOIN ON audits.user_id = utilisateurs.id', 'INNER JOIN audit_activities ON audit_activities.id = audits. audit_activity_id '],: order =>: sum_id,: group =>' users.id ') –

0

Il est assez facile d'obtenir vos utilisateurs, et itérer les audits pour chacun d'eux, en additionnant les valeurs que vous allez. Donc, ce serait quelque chose comme ceci:

 
users = User.find(:all) 
users.each do |user| 
    puts "user: #{user.email}" 
    score = 0 
    user.audits.each do |audit| 
     puts " audit: #{audit.audit_activity.id} score: #{audit.audit_activity.score_weight}" 
     score += audit.audit_activity.score_weight 
    end 
    puts "total score for this user: #{score" 
end 

Cela va générer beaucoup de requêtes séparées, cependant, ce n'est pas toujours une mauvaise chose. Si les volumes de données vont être grands, et comme vous le dites, vous voulez trier par score utilisateur, alors je pense que la réponse sera d'avoir un champ avec le score actuel sur le dossier de l'utilisateur, ce qui obtient mis à jour chaque fois qu'un enregistrement d'activité d'audit est écrit. Cela peut être effectué automatiquement avec un rappel d'association (c'est-à-dire une méthode after_add sur l'association d'audit dans l'enregistrement Utilisateur). Voir http://guides.rubyonrails.org/association_basics.html#association-callbacks.

+0

Merci stephenr, je pourrais certainement faire l'itération. Mon inquiétude était que l'itérer comme ça n'est peut-être pas une pratique idéale (je ne suis pas sûr, j'ai juste commencé avec RoR) parce que, comme vous l'avez deviné, les volumes de données seront très importants. Sur les rappels, ces scores ne sont valides que pour la journée en cours, donc je devrais les effacer à minuit chaque nuit. Peut-être une tâche de rake automatisée? –

+0

L'itération sur l'ensemble de données serait la manière normale de le faire, mais sera un problème si vous avez plusieurs dizaines de milliers d'enregistrements, et les récupérez tous en même temps. En ayant le score sur l'enregistrement de l'utilisateur, vous pouvez faire la recherche ordonnée par score, et utiliser un plugin de pagination ou une clause limite simple pour limiter le nombre d'enregistrements retournés. Rails sera très content de ça. Comme le travail cron pour effacer les vieux scores, un script exécuté par [RAILS_ROOT]/script/runner est probablement meilleur que rake - il vous donne accès à l'environnement complet de Rails, aux modèles, etc. – stephenr