Computing ActiveRecord nil attributes - ruby-on-rails

In my Rails app I have something like this in one of the models
def self.calc
columns_to_sum = "sum(price_before + price_after) as price"
where('product.created_at >= ?', 1.month.ago.beginning_of_day).select(columns_to_sum)
end
For some of the rows we have price_before and or price_after as nil. This is not ideal as I want to add both columns and call it price. How do I achieve this without hitting the database too many times?

You can ensure the NULL values to be calculated as 0 by using COALESCE which will return the first non NULL value:
columns_to_sum = "sum(COALESCE(price_before, 0) + COALESCE(price_after, 0)) as price"
This would however calculate the sum prices of all products.
On the other hand, you might not have to do this if all you want to do is have an easy way to calculate the price of one product. Then you could add a method to the Product model
def.price
price_before.to_i + price_after.to_i
end
This has the advantage of being able to reflect changes to the price (via price_before or price_after) without having to go through the db again as price_before and price_after will be fetched by default.
But if you want to e.g. select records from the db based on the price you need to place that functionality in the DB.
For that I'd modulize your scopes and join them again later:
def self.with_price
columns_to_sum = "(COALESCE(price_before, 0) + COALESCE(price_after, 0)) as price"
select(column_names, columns_to_sum)
end
This will return all records with an additional price reader method.
And a scope independent from the one before:
def self.one_month_ago
where('product.created_at >= ?', 1.month.ago.beginning_of_day)
end
Which could then be used like this:
Product.with_price.one_month_ago
This allows you to continue modifying the scope before hitting the DB, e.g. to get all Products where the price is higher than x
Product.with_price.one_month_ago.where('price > 5')

If you are trying to get the sum of price_before and price_after for each individual record (as opposed to a single sum for the entire query result), you want to do it like this:
columns_to_sum = "(coalesce(price_before, 0) + coalesce(price_after, 0)) as price"
I suspect that's what you're after, since you have no group in your query. If you are after a single sum, then the answer by #ulferts is correct.

Related

Rails, sum the aggregate of an attribute of all instances of a model

I have a model Channel. The relating table has several column, for example clicks.
So Channel.all.sum(:clicks) gives me the sum of clicks of all channels.
In my model I have added a new method
def test
123 #this is just an example
end
So now, Channel.first.test returns 123
What I want to do is something like Channel.all.sum(:test) which sums the test value of all channels.
The error I get is that test is not a column, which of course it is not, but I hoped to till be able to build this sum.
How could I achieve this?
You could try:
Channel.all.map(&:test).sum
Where clicks is a column of the model's table, use:
Channel.sum(:clicks)
To solve your issue, you can do
Channel.all.sum(&:test)
But it would be better to try achieving it on the database layer, because processing with Ruby might be heavy for memory and efficiency.
EDIT
If you want to sum by a method which takes arguments:
Channel.all.sum { |channel| channel.test(start_date, end_date) }
What you are talking about here is two very different things:
ActiveRecord::Calculations.sum sums the values of a column in the database:
SELECT SUM("table_name"."column_name") FROM "column_name"
This is what happens if you call Channel.sum(:column_name).
ActiveSupport also extends the Enumerable module with a .sum method:
module Enumerable
def sum(identity = nil, &block)
if block_given?
map(&block).sum(identity)
else
sum = identity ? inject(identity, :+) : inject(:+)
sum || identity || 0
end
end
end
This loops though all the values in memory and adds them together.
Channel.all.sum(&:test)
Is equivalent to:
Channel.all.inject(0) { |sum, c| sum + c.test }
Using the later can lead to serious performance issues as it pulls all the data out of the database.
Alternatively you do this.
Channel.all.inject(0) {|sum,x| sum + x.test }
You can changed the 0 to whatever value you want the sum to start off at.

update_all with a method

Lets say I have a model:
class Result < ActiveRecord::Base
attr_accessible :x, :y, :sum
end
Instead of doing
Result.all.find_each do |s|
s.sum = compute_sum(s.x, s.y)
s.save
end
assuming compute_sum is a available method and does some computation that cannot be translated into SQL.
def compute_sum(x,y)
sum_table[x][y]
end
Is there a way to use update_all, probably something like:
Result.all.update_all(sum: compute_sum(:x, :y))
I have more than 80,000 records to update. Each record in find_each creates its own BEGIN and COMMIT queries, and each record is updated individually.
Or is there any other faster way to do this?
If the compute_sum function can't be translated into sql, then you cannot do update_all on all records at once. You will need to iterate over the individual instances. However, you could speed it up if there are a lot of repeated sets of values in the columns, by only doing the calculation once per set of inputs, and then doing one mass-update per calculation. eg
Result.all.group_by{|result| [result.x, result.y]}.each do |inputs, results|
sum = compute_sum(*inputs)
Result.update_all('sum = #{sum}', "id in (#{results.map(&:id).join(',')})")
end
You can replace result.x, result.y with the actual inputs to the compute_sum function.
EDIT - forgot to put the square brackets around result.x, result.y in the group_by block.
update_all makes an sql query, so any processing you do on the values needs to be in sql. So, you'll need to find the sql function, in whichever DBMS you're using, to add two numbers together. In Postgres, for example, i believe you would do
Sum.update_all(sum: "x + y")
which will generate this sql:
update sums set sum = x + y;
which will calculate the x + y value for each row, and set the sum field to the result.
EDIT - for MariaDB. I've never used this, but a quick google suggests that the sql would be
update sums set sum = sum(x + y);
Try this first, in your sql console, for a single record. If it works, then you can do
Sum.update_all(sum: "sum(x + y)")
in Rails.
EDIT2: there's a lot of things called sum here which is making the example quite confusing. Here's a more generic example.
set col_c to the result of adding col_a and col_b together, in class Foo:
Foo.update_all(col_c: "sum(col_a + col_b)")
I just noticed that i'd copied the (incorrect) Sum.all.update_all from your question. It should just be Sum.update_all - i've updated my answer.
I'm completely beginner, just wondering Why not add a self block like below, without adding separate column in db, you still can access Sum.sum from outside.
def self.sum
x+y
end

Passing params to sql query

So, in my rails app I developed a search filter where I am using sliders. For example, I want to show orders where the price is between min value and max value which comes from the slider in params. I have column in my db called "price" and params[:priceMin], params[:priceMax]. So I can't write something kinda MyModel.where(params).... You may say, that I should do something like MyModel.where('price >= ? AND price <= ?', params[:priceMin], params[:priceMax]) but there is a problem: the number of search criteria depends on user desire, so I don't know the size of params hash that passes to query. Are there any ways to solve this problem?
UPDATE
I've already done it this way
def query_senders
query = ""
if params.has_key?(:place_from)
query += query_and(query) + "place_from='#{params[:place_from]}'"
end
if params.has_key?(:expected_price_min) and params.has_key?(:expected_price_max)
query += query_and(query) + "price >= '#{params[:expected_price_min]}' AND price <= '#{params[:expected_price_max]}'"
end...
but according to ruby guides (http://guides.rubyonrails.org/active_record_querying.html) this approach is bad because of SQL injection danger.
You can get the size of params hash by doing params.count. By the way you described it, it still seems that you will know what parameters can be passed by the user. So just check whether they're present, and split the query accordingly.
Edited:
def query_string
return = {}
if params[:whatever].present?
return.merge({whatever: #{params[:whatever]}}"
elsif ...
end
The above would form a hash for all of the exact values you're searching for, avoiding SQL injection. Then for such filters as prices you can just check whether the values are in correct format (numbers only) and only perform if so.

How to define reputation system values in model

How can I set up the default_scope in my blogging application so that the index orders the entries by an algorithm defined in the model?
If I were to use a HackerNews-like formula for the ranking algorithm as shown below, how can I define it in my model?
total_score = (votes_gained - 1) / (age_in_hours + 2)^1.5
The votes_gained variable relies on the Active_Record_Reputation_System, and is written as the following in my views:
votes_gained = #post.reputation_value_for(:votes).to_i
Finally, age_in_hours is pretty straight forward
age_in_hours = (Time.now - #post.created_at)/1.hour
How can I use these figures to order my blog posts index? I've been trying to figure out how to define total_score correctly in the model so that I can add it into the default scope as default_scope order("total_score DESC") or something similar. Direct substitution has not worked, and I'm not sure of how to "rephrase" each part of the formula.
How exactly should I define total_score? Thanks much for your insight!
Seeing as how you can't rely on active record to translate the formula into SQL, you have to write it yourself. The only potential concern here is that this is not a database-independent solution.
Since you are using Postgres, you can define your scope as (I haven't tested this yet, so let me know whether it works):
AGE_IN_HOURS = "(#{Time.now.tv_sec} - EXTRACT (EPOCH FROM posts.created_at))/3600"
TOTAL_SCORE = "(rs_reputations.value - 1)/((#{AGE_IN_HOURS}) + 2)^1.5"
default_scope joins("INNER JOIN rs_reputations ON rs_reputations.target_id = posts.id").where("rs_reputations.target_type = 'Post'").order(TOTAL_SCORE)
EDIT: Actually this won't work because, as it stands, Time.now is calculated one time (when the model loads), but it needs to be recalculated each time records are pulled. Use
default_scope lambda { order_by_score }
def self.order_by_score
age_in_hours = "(#{Time.now.tv_sec} - EXTRACT (EPOCH FROM posts.created_at))/3600"
total_score = "(rs_reputations.value - 1)/((#{age_in_hours}) + 2)^1.5"
joins("INNER JOIN rs_reputations ON rs_reputations.target_id = posts.id").where("rs_reputations.target_type = 'Post'").order(TOTAL_SCORE)
end

Order Mongoid results by dynamic, user-specific value

I have a Mongoid model, and I'd like to order the results by a score that's calculated between the model and current_user in real time. Suppose Thing has an instance method match_score:
def match_score(user) #can be user object too
score = 100
score -= 5 if user.min_price && !(price < user.min_price)
score -= 10 if user.max_price && !(price > user.max_price)
#... bunch of other factors...
return [score, 0].max
end
Is it possible to sort the results of any query by the value returned for a particular user?
MongoDB doesn't support arbitrary expressions in sorting. Basically, you only can specify a field and a direction (asc/desc).
With such complicated sorting logic as yours, the only way is to do it in the app. Or look at another data store.

Resources