问题
I have a method that computes stats (mainly sums) on a number of float attributes in a model.
The models
class GroupPlayer < ActiveRecord::Base
belongs_to :group
has_many :scored_rounds
has_many :rounds, dependent: :destroy
end
class Round < ActiveRecord::Base
belongs_to :group_player
end
class ScoredRound < Round
# STI
end
The method that provides stats on up to 4 float attributes that is called from a other methods, depending if I'm getting stats for one player or a group of players. An initial filter on ScoredRound is passed to the method (sr)
def method_stats(method,sr,grp)
rounds = sr.where.not(method => nil)
number_rounds = rounds.count
won = rounds.sum(method).round(2)
if method == :quality
dues = grp.options[:dues] * number_rounds
else
dues = grp.options["#{method.to_s}_dues"] * number_rounds
end
balance = (won - dues).round(2)
perc = dues > 0 ? (won / dues).round(3) : 0.0
[self.full_name,number_rounds,won,dues,balance,perc]
end
3 of the 4 attributes I am summing in ScoredRounds may not be set (nil) if the player did not win that game so the rounds are filtered.
Everything worked fine until I decided to add a limit on how many rounds to use. For instance if I only wanted status for the last 25 rounds in the query passed to method_stats
I'd call:
def money_stats(grp,method,limit=100)
sr = self.scored_rounds.where.not(method => nil).order(:date).reverse_order.limit(limit)
method_stats(method,sr,grp)
end
Again, I just added the limit and order clause to the query. Worked fine for all records.
If I simulate the procedure in the console with out using the above methods (or using them!) I'll get an erroneous sum
gp = GroupPlayer.find(123)
GroupPlayer Load (2.1ms) SELECT "group_players".* FROM "group_players" WHERE "group_players"."id" = $1 LIMIT $2 [["id", 123], ["LIMIT", 1]]
=> valid group player
sr = gp.scored_rounds.where.not(:quality => nil)
ScoredRound Load (1.7ms) SELECT "rounds".* FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> #<ActiveRecord::AssociationRelation [#<ScoredRound id: 5706, player_id: 123, group_player_id: 123, event_id: 12, type: "ScoredRound", date: "2016-11-04", team: 3, tee: "White", quota: 32, front: 15, back: 15, total: 30, created_at: "2016-11-04 14:18:27", updated_at: "2016-11-04 19:12:47", quality: 0.0, skins: nil, par3: nil, other: nil>,...]
sr.count
(1.5ms) SELECT COUNT(*) FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 44
sr.sum(:quality)
(1.0ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 354.166666666667
# Now if I add the order and limit clause
sr = gp.scored_rounds.where.not(:quality => nil).order(:date).reverse_order.limit(25)
ScoredRound Load (1.6ms) SELECT "rounds".* FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) ORDER BY "rounds"."date" DESC LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> => #<ActiveRecord::AssociationRelation [...]
sr.count
(1.1ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2) subquery_for_count [["group_player_id", 123], ["LIMIT", 25]]
=> 25
sr.sum(:quality)
(1.8ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> 354.166666666667
### This is the error, it return the sum off all records,
# not the limited???? if I use pluck and sum
sr.pluck(:quality)
=> [10.0, 11.3333333333333, 10.0, 34.0, 0.0, 7.33333333333333, 0.0, 0.0, 31.5, 0.0, 21.3333333333333, 0.0, 19.0, 0.0, 0.0, 7.5, 0.0, 20.0, 10.0, 28.0, 8.0, 9.5, 0.0, 3.0, 24.0]
sr.pluck(:quality).sum
=> 254.49999999999994
Don't know if I found a bug in AREL or I'm doing something wrong. I tried it with just Round instead of the STI ScoredRound with the same results.
Any ideas?
回答1:
If you notice, the SUM
results for both, with and without LIMIT
, are the same:
sr = gp.scored_rounds.where.not(:quality => nil)
sr.sum(:quality)
(1.0ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 354.166666666667
sr = gp.scored_rounds.where.not(:quality => nil).order(:date).reverse_order.limit(25)
sr.sum(:quality)
(1.8ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> 354.166666666667
That's because LIMIT
affects the number of rows returned by the query and SUM
returns just one, so the function is applied for all the 44 records, not the 25 given to LIMIT
. That's not what happens with sr.pluck(:quality).sum
which applies only to the 25 records returned by the query.
Don't know if I found a bug in AREL or I'm doing something wrong
Sadly, 99.9% of times is not a bug but our fault :(
回答2:
# File activerecord/lib/active_record/relation/calculations.rb, line 75
def sum(column_name = nil)
return super() if block_given?
calculate(:sum, column_name)
end
if you call sr.sum(:quality)
then sum take quality as a column name and Calculates the sum of values on a given column.
来源:https://stackoverflow.com/questions/43955310/rails-sum-on-associationrelation-attribute-is-incorrect-if-association-has-limit