Reputation: 15181
I have a model Company
that have columns pbr
, market_cap
and category
.
To get averages of pbr
grouped by category
, I can use group
method.
Company.group(:category).average(:pbr)
But there is no method for weighted average.
To get weighted averages I need to run this SQL code.
select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";
In psql
this query works fine. But I don't know how to use from Rails.
sql = %q(select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";)
ActiveRecord::Base.connection.select_all(sql)
returns a error:
output error: #<NoMethodError: undefined method `keys' for #<Array:0x007ff441efa618>>
It would be best if I can extend Rails method so that I can use
Company.group(:category).weighted_average(:pbr)
But I heard that extending rails query is a bit tweaky, now I just want to know how to run the result of sql from Rails.
Does anyone knows how to do it?
rails: 4.2.1
Upvotes: 1
Views: 494
Reputation: 24581
What version of Rails are you using? I don't get that error with Rails 4.2. In Rails 3.2 select_all
used to return an Array
, and in 4.2 it returns an ActiveRecord::Result
. But in either case, it is correct that there is no keys
method. Instead you need to call keys
on each element of the Array
or Result
. It sounds like the problem isn't from running the query, but from what you're doing afterward.
In any case, to get the more fluent approach you've described, you could do this:
class Company
scope :weighted_average, lambda{|col|
select("companies.category").
select(<<-EOQ)
(CASE WHEN SUM(market_cap) = 0 THEN 0
ELSE SUM(#{col} * market_cap) / SUM(market_cap)
END) AS weighted_average_#{col}
EOQ
}
This will let you say Company.group(:category).weighted_average(:pbr)
, and you will get a collection of Company
instances. Each one will have an extra weighted_average_pbr
attribute, so you can do this:
Company.group(:category).weighted_average(:pbr).each do |c|
puts c.weighted_average_pbr
end
These instances will not have their normal attributes, but they will have category
. That is because they do not represent individual Companies, but groups of companies with the same category
. If you want to group by something else, you could parameterize the lambda to take the grouping column. In that case you might as well move the group
call into the lambda too.
Now be warned that the parameter to weighted_average
goes straight into your SQL query without escaping, since it is a column name. So make sure you don't pass user input to that method, or you'll have a SQL injection vulnerability. In fact I would probably put a guard inside the lambda, something like raise "NOPE" unless col =~ %r{\A[a-zA-Z0-9_]+\Z}
.
The more general lesson is that you can use select
to include extra SQL expressions, and have Rails magically treat those as attributes on the instances returned from the query.
Also note that unlike with select_all
where you get a bunch of hashes, with this approach you get a bunch of Company
instances. So again there is no keys
method! :-)
Upvotes: 1