Getting the percentage of a relationship - ruby-on-rails

Orders have many line_items, which in turn have an employee_id and amount_cents. In the second line of this query, I try creating a line_item_perc variable by gathering all the line_items with a certain employee_id and dividing their amounts_cents by all the other line_items:
Order.joins(:tips, :line_items)
.select('line_items WHERE(custom_attributes -> "employee_id" = ?) AS employee_line_items,
SUM(employee_line_items.amount_cents) / SUM(line_items.amount_cents) AS line_item_perc',
params[:employee_id])
.sum('tips.amount_cents * line_item_perc')
Unfortunately, this doesn't work. The select statement doesn't even appear in the final SQL. (I'm assuming this is because sum overwrites it.) So what else could I do to get line_item_perc?

def line_item_percent(employee_id, line_item_id)
total_amount_in_cents = Order.joins(:line_items).where("employee_id = ?", employee_id).pluck(:amount_cents).sum
line_item_cents = LineItem.find(line_item_id).amount_cents
answer = line_item_cents / total_amount_in_cents
end #returns decimal
Try this. Did it off top of head with rails not SQL.

Related

How to get a unique set of parent models after querying on child

Order has_many Items is the relationship.
So let's say I have something like the following 2 orders with items in the database:
Order1 {email: alpha#example.com, items_attributes:
[{name: "apple"},
{name: "peach"}]
}
Order2 {email: beta#example.com, items_attributes:
[{name: "apple"},
{name: "apple"}]
}
I'm running queries for Order based on child attributes. So let's say I want the emails of all the orders where they have an Item that's an apple. If I set up the query as so:
orders = Order.joins(:items).where(items: {name:"apple"})
Then the result, because it's pulling at the Item level, will be such that:
orders.count = 3
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com", "beta#example.com"]
But my desired outcome is actually to know what unique orders there are (I don't care that beta#example.com has 2 apples, only that they have at least 1), so something like:
orders.count = 2
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
How do I do this?
If I do orders.select(:id).distinct, this will fix the problem such that orders.count == 2, BUT this distorts the result (no longer creates AR objects), so that I can't iterate over it. So the below is fine
deduped_orders = orders.select(:id).distinct
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
But then the below does NOT work:
deduped_orders.each do |o|
puts o.email # ActiveModel::MissingAttributeError: missing attribute: email
end
Like I basically want the output of orders, but in a unique way.
I find using subqueries instead of joins a bit cleaner for this sort of thing:
Order.where(id: Item.select(:order_id).where(name: 'apple'))
that ends up with this (more or less) SQL:
select *
from orders
where id in (
select order_id
from items
where name = 'apple'
)
and the in (...) will clear up duplicates for you. Using a subquery also clearly expresses what you want to do–you want the orders that have an item named 'apple'–and the query says exactly that.
use .uniq instead of .distinct
deduped_orders = orders.select(:id).uniq
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
If you want to keep all the attributes of orders use group
deduped_orders = orders.group(:id).distinct
deduped_orders.each do |o|
puts o.email
end
#=> output: "alpha#exmaple.com", "beta#example.com"
I think you just need to remove select(:id)
orders = Order.joins(:items).where(items: {name:"apple"}).distinct
orders.pluck(:email)
# => ["alpha#exmaple.com", "beta#example.com"]
orders = deduped_orders
deduped_orders.each do |o|
puts o.email # loop twice
end

Increase performance: avoid looking for the right element in a collection

I have this situation.
activity.rb
belongs_to :user
belongs_to :cause
belongs_to :sub_cause
belongs_to :client
def amount
duration / 60.0 * user.hourly_cost_by_year(date.year).amount rescue 0
end
user.rb
has_many :hourly_costs # one hourly_cost for year
has_many :activities
def hourly_cost_by_year(year = Date.today.year)
hourly_costs.find { |hc| hc.year == year }
end
hourly_cost.rb
belongs_to :user
I have a big report where I achieved good performance (the number of SQL queries is fixed) but I think I could do better. The query I use is
activities = Activity.includes(:client, :cause, :sub_cause, user: :hourly_costs)
And this is ok, it's fast, but I think is improvable because hourly_cost_by_year method. I mean, activity has a date and I can use that date to know which of those hourly costs I should use. Something like this in activity
def self.user_with_single_hourly_cost
joins('LEFT JOIN users u ON u.id = activities.user_id').
joins('LEFT JOIN hourly_costs hc ON hc.user_id = u.id AND hc.year = EXTRACT(year from activities.date)')
end
But I don't how integrate this in my query. Whatever I tried did not work. I could use raw SQL but I'm trying to use ActiveRecord. I even thought to use redis to cache every hourly cost by user and year, could work, but I think this query, with the extract part, should do the best job because I'd have a flat table.
Update: I try to clarify. Whatever query I use in my action at some point I have to do
activities.sum(&:amount)
and that method, you know, is
def amount
duration / 60.0 * user.hourly_cost_by_year(date.year).amount rescue 0
end
And I don't know how to pick directly the hourly_cost I want without search between hourly_costs. Is this possible?
You may consider using Arel for this. Arel is the underlying query assembler for rails/activerecord (so no new dependencies) and can be very useful when building complex queries because it offers far more depth than the high level ActiveRecord::QueryMethods.
Obviously with a broader API comes more verbosity (which actually adds quite a bit to the readability) and less syntactical sugar which takes some getting used to but has proven indispensable for me on multiple occasions.
While I did not take the time to recreate your data structure something like this may work for you
activities = Activity.arel_table
users = User.arel_table
hourly_costs = HourlyCost.arel_table
activity_users_hourly_cost = activities
.join(users,Arel::Nodes::OuterJoin)
.on(activities[:user_id].eq(users[:id]))
.join(hourly_costs,Arel::Nodes::OuterJoin)
.on(hourly_costs[:user_id].eq(users[:id])
.and(hourly_costs[:year].eq(Arel::Nodes::Extract.new(activities[:date],'year'))
)
)
Activity.includes(:client, :cause, :sub_cause).joins(activity_users_hourly_cost.join_sources)
This will add the requested join e.g.
activity_users_hourly_cost.to_sql
#=> SELECT
FROM [activities]
LEFT OUTER JOIN [users] ON [activities].[user_id] = [users].[id]
LEFT OUTER JOIN [hourly_costs] ON [hourly_costs].[user_id] = [users].[id]
AND [hourly_costs].[year] = EXTRACT(YEAR FROM [activities].[date])
Update
If you just want to add the "hourly_cost" this should work for you
Activity.includes(:client, :cause, :sub_cause)
.joins(activity_users_hourly_cost.join_sources)
.select("activities.*, activities.duration / 60.0 * ISNULL([hourly_costs].[amount],0) as hourly_cost_by_year")
Please note that this will only return Activity objects but they will now have a method called hourly_cost_by_year which will return the result of that calculation. Full SQL will look like
SELECT
[activities].*,
activities.duration / 60.0 * ISNULL([hourly_costs].[amount],0) as hourly_cost_by_year
FROM [activities]
-- Dependant upon WHERE Clause
LEFT OUTER JOIN causes ON [activities].[cause_id] = [causes].[id]
LEFT OUTER JOIN sub_causes ON [activities].[subcause_id] = [subcauses].[id]
LEFT OUTER JOIN clients [activities].[client_id] = [clients].[id]
--
LEFT OUTER JOIN [users] ON [activities].[user_id] = [users].[id]
LEFT OUTER JOIN [hourly_costs] ON [hourly_costs].[user_id] = [users].[id]
AND [hourly_costs].[year] = EXTRACT(YEAR FROM [activities].[date])
You could build the select portion in Arel too if you like but seems overkill for such a simple statement.

Rails show total column when data comes from model method

I have a model Invoices
Table Invoices:
ID, PRODUCT, UNITS, PRICE
In my view, I want to show total for some columns.
For instance to show total quantity I can set in my controller:
#invoices = Invoice.group(:PRODUCT).select(ID, PRODUCT, UNITS, SUM(UNITS) AS TOTALUNITS, PRICE").order('PRODUCT ASC')
#units_total = #invoices.map(&:UNITS).sum
and in my view
<%= #units_total %>
and it returns the total in column UNITS. This works fine.
If I define in my model:
def total_amount
(self.PRICE * self.TOTALUNITS)
end
and then on the same way I want to show the total of total_amount, I tried in my controller:
#amount_total = #invoices.map(&:total_amount).sum
it doesn't work, as I assume that if I'm not using a column name the syntax must be different.
What should I enter then?
UPDATE
The problem comes form the fact that I'm using in model (self.PRICE * self.TOTALUNITS). I didn't include it when I posted the question as I thought it didn't matter. But if I replace(self.PRICE * self.TOTALUNITS) with (self.PRICE * self.UNITS) there's no error but values are obviously wrong.
What you have taken is correct, I think some of the invoices doesnot have the value, check using try,
def total_amount
price = self.try(:PRICE) || 0
units = self.try(:UNITS) || 0
price * units
end
make 0 if the corresponding field value is absent, so that you will not get an error.
You can check if this is working or not
#amount_total = #invoices.map{ |invoice| invoice.PRICE.to_f * invoice.UNITS.to_i }.sum
I don't know what error you are getting but it might be due to null values in the column.
Hope that helps!

How do I get the first row in a record after ordering it? Ruby On Rails

I am working with Ruby on Rails.
I make a query to the model, but I want to get the one register that has the highest value for the average attribute. This is my code:
#dish = Dish.where("day = ? and week = ?", params[:day], params[:week])
#dish.order(:average)
#sug = #dish.first
#sug gets the record with the lowest id, no the one with the highest average.
I have also tried it this way:
#sug = #dish.order(:average).limit(1)
but it's not working either. How can I get that one register?
You need to chain the calls like follows.
#dish.order(:average).first
The call to .order does not change the #dish instance, so when you call #dish.first the ordering no longer is there.
#dish.order does not order already found dishes, it changes the query to include an order clause.
#dish = Dish.where("day = ? and week = ?", params[:day], params[:week]).order(:average) would do what you wanted, and so would
#dish = Dish.where("day = ? and week = ?", params[:day], params[:week])
#dish = #dish.order(:average)
#sug = #dish.first
The ActiveRecord::QueryMethods#order method sorts in ascending order by default, so try explicitly asking for a descending order, then taking the first:
Dish.order(average: :desc)
.where("day = ? and week = ?", params[:day], params[:week])
.first
This assumes your average attribute is stored in the database, and not computed.

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources