How to group and sum by foreign key? - ruby

I have these two models in my Rails app:
class Person < ApplicationRecord
has_many :payments
end
class Payment < ApplicationRecord
belongs_to :person
end
How can I group the payments by person and order them by amount?
Right now I have...
Payment.group(:person_id).sum("amount")
...which works but doesn't include the persons' names. It returns something like this:
{ 1 => 1200.00, 2 => 2500.00 }
How can I replace the IDs / integers with the persons' names and also sort the whole thing by amount?
Thanks for any help!

Just be a bit more specific:
Payment.select('people.name, SUM(payments.amount)').joins(:person).group(:person_id)
Assuming that the persons table is named people in your application.

This will return the ActiveRecord::Relation that you can work with:
Person.joins(:payments).group('persons.id').select("persons.id, persons.name, sum(payments.amount) as amounts_summ")
Only for unique name fields:
Assuming you have name property for Person model, solution can be like this:
Payment.joins(:person).group(:name).order('sum_amount DESC').sum(:amount)
It generates query
SELECT SUM("payments"."amount") AS sum_amount, "name" AS name FROM "payments" INNER JOIN "persons" ON "persons"."id" = "payments"."persons_id" GROUP BY "name" ORDER BY sum_amount DESC
and return hash like this:
=> {"Mike"=>22333.0, "John"=>5676.0, "Alex"=>2000.0, "Carol"=>2000.0}

Related

Take total price from all product for many users

i have issue
Model Driver has association like has_many: orders
class Driver < User
has_many :orders
end
and Order has belongs_to :user
class Order < ActiveRecord::Base
belongs_to :user
end
and Order has column like price
My problem is, i want to display all drivers by table
first column will be first_name, next last_name,
i want to display total price for each users. price will be counting by summing by all orders for user,
Problem is n+1, how i can display total price for each user, without sent separate request to the DB
Example of index page
first_name.
last_name
Price
Arian
Lain
2500
Brain
Kokun
4700
You can get a sum for grouped values using a single SQL query with SUM and GROUP BY. In Rails, this can look like this:
#sums = Order.group_by(:user_id).sum(:price)
# {1 => 2500, 2 => 4700, ...}
In your view, you can then fetch the sum for the respective user using the user's id as a key, e.g. with this (assuming you have the current driver / user in the driver variable):
<%= #sums[driver.id] %>

How to order by integer returned by instance method? [duplicate]

Is there anyway I can order the results (ASC/DESC) by number of items returned from the child model (Jobs)?
#featured_companies = Company.joins(:jobs).group(Job.arel_table[:company_id]).order(Job.arel_table[:company_id].count).limit(10)
For example: I need to print the Companies with highest jobs on top
Rails 5+
Support for left outer joins was introduced in Rails 5 so you can use an outer join instead of using counter_cache to do this. This way you'll still keep the records that have 0 relationships:
Company
.left_joins(:jobs)
.group(:id)
.order('COUNT(jobs.id) DESC')
.limit(10)
The SQL equivalent of the query is this (got by calling .to_sql on it):
SELECT "companies".* FROM "companies" LEFT OUTER JOIN "jobs" ON "jobs"."company_id" = "companies"."id" GROUP BY "company"."id" ORDER BY COUNT(jobs.id) DESC
If you expect to use this query frequently, I suggest you to use built-in counter_cache
# Job Model
class Job < ActiveRecord::Base
belongs_to :company, counter_cache: true
# ...
end
# add a migration
add_column :company, :jobs_count, :integer, default: 0
# Company model
class Company < ActiveRecord::Base
scope :featured, order('jobs_count DESC')
# ...
end
and then use it like
#featured_company = Company.featured
#user24359 the correct one should be:
Company.joins(:jobs).group("companies.id").order("count(companies.id) DESC")
Something like:
Company.joins(:jobs).group("jobs.company_id").order("count(jobs.company_id) desc")
Added to Tan's answer. To include 0 association
Company.joins("left join jobs on jobs.company_id = companies.id").group("companies.id").order("count(companies.id) DESC")
by default, joins uses inner join. I tried to use left join to include 0 association
Adding to the answers, the direct raw SQL was removed from rails 6, so you need to wrap up the SQL inside Arel (if the raw SQL is secure meaning by secure avoiding the use of user entry and in this way avoid the SQL injection).
Arel.sql("count(companies.id) DESC")
Company.where("condition here...")
.left_joins(:jobs)
.group(:id)
.order('COUNT(jobs.id) DESC')
.limit(10)

How can I avoid running singular expressions against arrays in activerecord and rails 3?

I am sorry if I am asking the question poorly. I have a Rails 3.1 app with models (simplified) like so:
class Employee < ActiveRecord::Base
has_many :merged_children, :class_name => 'Employee', :foreign_key => "merge_parent_id"
has_many :timesheets
def total_time
merged_children.timesheets.in_range(range).hours_minutes.sum
end
end
class Timesheet < ActiveRecord::Base
belongs_to :employee
def in_range(range)
# filter records based on transaction_date in range
end
def hours_minutes
(hours + minutes/60.0).to_f
end
end
Note: The in_range method acts as a scope, essentially, and hours_minutes is a calculation. hours_minutes is valid for each timesheet record in the resulting dataset, and then total_time should sum those values and return the amount.
The "total_time" method is not working because employee.merged_children returns an array and timesheets is meant to run against a single Employee object.
Is there any way to structure the "total_time" so that it still sends one query to the db? It seems inelegant to iterate over the merged_children array, issuing a query for each. Not sure if a direct call to an Arel table would help or hurt, but I am open to ideas.
If we get it right, the resulting SQL should effectively look something like:
SELECT sum(hours + minutes/60.0)
FROM employees e1 join employees e2 on e1.id = e2.merge_parent_id join timesheets t on t.employee_id = e2.id
WHERE e1.id = [#employee.id] and t.transaction_date BETWEEN [#range.begin] and [#range.end]
Thanks so much!
The easiest thing here might be to add
has_many :children_timesheets, :through => :merged_children, :source => :timesheets
To your employee model,
Then (assuming in_range is actually a scope, or a class method that does a find)
children_timesheets.in_range(...)
Should be the collection of timesheets you're interested in and you can do something like
children_timesheets.in_range(...).collect(&:hours_minutes).sum
Untested with actual data.
range = ((1.day.ago)...(2.days.ago))
merge_parent = Employee.find(some_id)
Timesheet.where(:transaction_date => range)
.joins(:employee).where(:employees => {:merge_parent_id => merge_parent.id})
.sum('hours*60 + minutes')
(0.3ms) SELECT SUM(hours*60 + minutes) AS sum_id FROM "timesheets" INNER JOIN "employees" ON "employees"."id" = "timesheets"."employee_id" WHERE "employees"."merge_parent_id" = 1 AND ("timesheets"."created_at" >= '2011-12-13 03:04:35.085416' AND "timesheets"."created_at" < '2011-12-12 03:04:
Returns "0" for me. So hopefully it will return something nicer for you

Optimizing nested activerecord query

Activerecord question.. How to optimize this query..
Prefectures (have many) Cities (have many) Shops (have many) Sales (have many) Brands
I'd like to get a list of one sale per prefecture, which is yet to end.. and then list the brands available at the sale.
The nesting is making this kind of tricky for me!
Here is what I came up with, though it's pretty ugly & I think it can be optimized at the query level rather than getting all unfinished sales..
#Get all sales which are yet to finish, ordered by finish date
upcoming_sales = Sale.includes([{:shop => {:city => :prefecture}}, :brands])
.where("finish > ?", Date.today)
.order('start ASC, finish ASC')
.select(['shop.city.prefecture.name', 'brands.name'])
#filter down to a single sale per prefecture
#sales = upcoming_sales.each_with_object({}){ |s,o|
o[s.shop.city.prefecture.name] = o[s.shop.city.prefecture.name] ||= s
}
How about something like this?
class Sale < ActiveRecord::Base
belongs_to :shop
has_many :brands
def self.first_sale_per_prefecture()
first_sale_id_per_prefecture = %(
select max(sales.id)
from sales
inner join shops on shop_id = shops.id
inner join cities on city_id = cities.id
where finish > #{Date.today}
group by prefecture_id
order by finish desc)
where("sales.id in (#{first_sale_id_per_prefecture})").includes(:brands, :shop => {:city => :prefecture})
end
end
You could get the upcoming sales and then join to shops => cities => prefectures and SELECT DISTINCT prefecture_id
this would ensure you only have one sale per prefecture. Something like this:
#sales = Sale.includes([{:shop => :prefecture},:brands])
.order('finish DESC')
.where("finish > ?", Date.today)
.joins(:shops => { :cities => :prefectures })
.select('sales.*, DISTINCT prefecture.id')
I'm going to try this using Arel
class Sale < ActiveRecord::Base
belongs_to :shop
class << self
# Returns only Sale objects with obj.finish > today
# add on other ActiveRecord queries:
# Sale.unfinished.all
# Sale.unfinished.all :limit => 10
def unfinished
where(Sale.arel_table[:finish].gt(Date.today))
end
# should select only one set of unfinished sales,
# and where the prefecture name is distinct
def distinct_prefecture
Sale.unfinished.joins({:shop => {:city => :prefecture}}).where(Prefecture.arel_table[:name].distinct)
end
end
end
Then, where you want it:
#sales = Sale.distinct_prefecture \
.includes(:brands]) \ # should already include the other stuff with joins
.order('start ASC, finish ASC')
#brand_list = #sales.collect{|s| s.brands}
If you want a limited result, this should be ok:
#sales = Sale.distinct_prefecture \
.limit(10) \
.includes(:brands]) \
.order('start ASC, finish ASC')

ROR- Cannot use Find in a 1-many relationship

In Ruby on rails, our model includes orders and payments.
There's 1-many relationship between order and payments.
In the orders model we specify:
has_many :payments, :as => :payable
And a payment record has payable_id that is set to order.id.
In a report, I want to select all payments that belong to orders of a given type.
Using:
payments = Payment.find(:all, :conditions => conditions)
and adding 'payable.type="a" ' to the conditions doesn't work.
It seems that ActiveRecord doesn't develop this into a correct join statement (payable_id=order.id and orders.type='a').
I cannot use explicit SQL here, as the condition contains other things that are inserted there earlier in the code.
Thanks,
Raffi Lipkin
Your conditions clause is wrong.
You state that an Order
has_many :payments, :as => :payable
This tells me that a Payment
belongs_to :payable, :polymorphic => true
This means that the payments table has two columns of note: payable_id and payable_type. This also means that Payments can be applied not just to Orders, but also to other models as well (CreditCardBalances, who knows).
If you want to query for payments of a specific type, i.e. belonging to any instance of a particular class, you need to be querying the field payments.payable_type. This works fine:
Payment.find(:all, :conditions => "payable_type = 'Order'")
Here's a gist that shows what I did to test this. The models created are set up just like described above.
Don't forget that you can extract that into named scopes if it's easier:
named_scope :on_orders, :conditions => "payable_type = 'Order'"
Which makes it
Payment.on_orders
Or dynamically:
named_scope :on, lambda { |type| { :conditions => "payable_type = '#{type.to_s}'" } }
Which then makes it
Payment.on(Order) # or Payment.on(CreditCardBalance) or Payment.on("Order")
Try incliding and reference the actual table id name in the condition, rather than the association alias:
find(:include => "payments", :conditions => ["payment.type = ?", "x"]
You mention 'payment type'. If they're fairly static, have you considered using single table inheritance (STI) to subclass your different payment types? Then Rails will do all the magic to filter on type.
E.g.
class CreditCardPayment < Payment
...
end
It doesn't even need to exhibit different behaviour initially; however you'll probably find that it turns out to be really useful to have different data and polymorphic behaviour around payments.

Resources