PSQL Query summing columns on a "has many through" relationship yielding duplicates - ruby-on-rails

My goal is to generate an ActiveRecordRelation containing each Customer with it's balance (sale_line_items.total + sale_adjustments.effect_to_balance - sale_payment.amount)
class Customer < ActiveRecord:Base
has_many :sales
class Sale < ActiveRecord:Base
belongs_to :customer
has_many :sale_adjustments
has_many :sale_payments
class SaleLineItem < ActiveRecord:Base
belongs_to :sale
class SalePayment < ActiveRecord:Base
belongs_to :sale
class SaleAdjustment < ActiveRecord:Base
belongs_to :sale
My current code:
customers = Customer.all
customers = customers.joins("LEFT OUTER JOIN sales ON customers.id = sales.customer_id")
customers = customers.joins("LEFT OUTER JOIN sale_line_items ON sale_line_items.sale_id = sales.id")
customers = customers.joins("LEFT OUTER JOIN sale_adjustments ON sale_adjustments.sale_id = sales.id")
customers = customers.joins("LEFT OUTER JOIN sale_payments ON sale_payments.sale_id = sales.id")
customers = customers.select("customers.*,
COALESCE(SUM(sale_line_items.total),0) +
COALESCE(SUM(sale_adjustments.effect_to_balance),0) -
COALESCE(SUM(sale_payments.amount),0) AS customer_balance")
customers.group("customers.id").distinct
The problem is that if there are more than one payments or adjustments, the line items are duplicated in the resulting table, essentially increasing the total by a factor of that number. I think i understand enough to identify the problem, but not enough to come up with a solution. The rails generated query is below.
[1m[36mCustomer Load (2.2ms)[0m [1mSELECT DISTINCT customers.*,
COALESCE(SUM(sale_line_items.total),0) +
COALESCE(SUM(sale_adjustments.effect_to_balance),0) -
COALESCE(SUM(sale_payments.amount),0) AS customer_balance
FROM "customers" LEFT OUTER JOIN sales ON customers.id = sales.customer_id
LEFT OUTER JOIN sale_line_items ON sale_line_items.sale_id = sales.id
LEFT OUTER JOIN sale_adjustments ON sale_adjustments.sale_id = sales.id
LEFT OUTER JOIN sale_payments ON sale_payments.sale_id = sales.id
WHERE "customers"."business_id" = $1 GROUP BY customers.id[0m [["business_id", "bd0c474c-db6e-43bc-95ca-90541d3840d1"]]
An example of the error is:
A customer with one sale that has one line item of $50
total sales: $50, balance: $50
Add one payment of $10
total sales: $50, balance: $40
Problems occur on the second payment, lets say $1
total sales $100, balance $89
That extra payment creates a duplicate row for the line item. It will go to $150 on a third payment and so on...
Any help is greatly appreciated - i've been working on this all night and at this point i'm just going around in circles.

I have worked on something similar while I was calculating invoice balance for a PHP-MySQL project. I ended up changing my database structure and application flow. I modified the invoices table to include invoice_total and total_payment columns which will be updated with creation and edition of Invoice and Payment modules.
Although you can use sub-queries to get the desired result like:
SELECT DISTINCT customers.*,
COALESCE(SUM(sli.total),0) +
COALESCE(SUM(sa.effect_to_balance),0) -
COALESCE(SUM(sp.amount),0) AS customer_balance
FROM "customers" LEFT OUTER JOIN sales ON customers.id = sales.customer_id
LEFT OUTER JOIN (SELECT sale_id, SUM(total) total FROM sale_line_items) sli ON sli.sale_id = sales.id
LEFT OUTER JOIN (SELECT sale_id, SUM(effect_to_balance) effect_to_balance FROM sale_adjustments) sa ON sa.sale_id = sales.id
LEFT OUTER JOIN (SELECT sale_id, SUM(amount) amountFROM sale_payments) sp ON sp.sale_id = sales.id
WHERE "customers"."business_id" = $1 GROUP BY customers.id
Hope this will solve your problem :)

The rails code modeled after Manoj Monga's answer:
customers = Customer.all
customers = customers.joins("LEFT OUTER JOIN sales ON customers.id = sales.customer_id")
customers = customers.joins("LEFT OUTER JOIN (SELECT sale_id, SUM(total) total FROM sale_line_items GROUP BY sale_id) sli ON sli.sale_id = sales.id")
customers = customers.joins("LEFT OUTER JOIN (SELECT sale_id, SUM(effect_to_balance) effect_to_balance FROM sale_adjustments GROUP BY sale_id) sa ON sa.sale_id = sales.id")
customers = customers.joins("LEFT OUTER JOIN (SELECT sale_id, SUM(amount) amount FROM sale_payments GROUP BY sale_id) sp ON sp.sale_id = sales.id")
customers = customers.select("customers.*,
COALESCE(SUM(total), 0) as sales_total,
COALESCE(SUM(total), 0) +
COALESCE(SUM(effect_to_balance),0) -
COALESCE(SUM(amount),0) AS customer_balance")
customers.group("customers.id").distinct

Related

having in ActiveRecord

I have been trying to find a solution to my problem for a few days, so I am turning towards the community, hopefully I am not missing something obvious here.
I have 2 models in rails:
class Room
has_many :accesses
end
class Access
belongs_to :accessor, polymorphic: true
end
Accessor can be of 2 types: Person or Team
I am trying to find the most efficient way to find the rooms that a user has access to, but which are not accessible from any teams.
I tried:
Room.joins(:accesses).where(accesses: {accessor: Person.find(1234)}).where.not(accesses: {accessor_type: Team'})
But that returns the rooms that people have accesses to, it does not filter out the ones that Team AND People have access to.
I am thinking the having clause is the way to go, in which it would count the number of Teams accesses to rooms, and keep the rooms that have 0 team accesses. Though all my attempts are failing.
I would love to hear any advice.
Left join
Instead of using HAVING, which requires us to add a GROUP BY, I'd start with a LEFT JOIN and a WHERE.
You can do this by left-joining to the room_accesses table specifically on "Team" accessor_type. We're left-joining because we're going to scope this join to only team accesses, and select only the rows where no such accesses exist. An inner join would not return these rows at all. We'll need to use a table alias as we're already using the room_accesses table to join to the person you are looking up.
We may as well admit Rails isn't great at this level of query abstraction, so let's just construct the raw SQL fragments for our first solution:
person = Person.find(1234)
person.rooms.joins(
"LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'"
).where("team_accesses.id IS NULL")
This generates, for SQLite,
SELECT "rooms".* FROM "rooms"
INNER JOIN "room_accesses"
ON "rooms"."id" = "room_accesses"."room_id"
LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'
WHERE "room_accesses"."accessor_id" = 1
AND "room_accesses"."accessor_type" = 'Person'
AND (team_accesses.id IS NULL)
Having
You can do this with aHAVING by similarly joining to room_accesses again with the team_accesses alias, grouping by rooms.id (since we want at most one record per room), and selecting the groups HAVING a zero count of team accesses:
person.rooms.joins(
"LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'"
).group("rooms.id").having("COUNT(team_accesses.id) = 0")
generates:
SELECT "rooms".* FROM "rooms"
INNER JOIN "room_accesses"
ON "rooms"."id" = "room_accesses"."room_id"
LEFT JOIN room_accesses team_accesses
ON team_accesses.room_id = rooms.id
AND team_accesses.accessor_type = 'Team'
WHERE "room_accesses"."accessor_id" = 1
AND "room_accesses"."accessor_type" = 'Person'
GROUP BY rooms.id
HAVING (COUNT(team_accesses.id) = 0)
Using associations instead of raw SQL
You can get halfway there in Rails by defining a scoped association:
class Room < ApplicationRecord
has_many :room_accesses
has_many :team_accesses, ->{ where accessor_type: "Team" }, class_name: "RoomAccess"
end
Assuming you're using a recent version of ActiveRecord, this allows you to do
person.rooms.left_joins(:team_accesses)
However, the table name used for this left joins is "team_accesses_rooms", which is predictable in this simple case but not part of the public API to my knowledge and subject to being changed if other joins are used in this same query. Still, if you're feeling daring:
person.rooms.left_joins(:team_accesses).where(team_accesses_rooms: {id: nil})
Frankly I would not recommend this method as you're relying on a table alias that you're not in control of and is not obvious where it comes from. With the raw SQL, you are in control of it and it's obvious where it came from.

Respect negative conditions for advanced collection associations

I was trying to use this functionality introduced in #645 with conditional 2nd degree has_many ... through relationships with little success.
In my case:
a Course has_many :user_assigned_content_skills, -> { where(source: 'user') }, class_name: "ContentSkill"
and a ContentSkill belongs_to :skill and belongs_to :course
Then Course.ransack({user_assigned_content_skills_skill_name_not_cont: 'ruby'}).result.to_sql returns the following:
"SELECT courses.* FROM courses LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user' LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id WHERE (skills.name NOT ILIKE '%ruby%')"
This means false positives again if a course has multiple content_skills. Any ideas how to retrieve all courses not being associated with a given skill name?
Many thanks for any insights!
You can get ids of courses associated with a given skill name, and then get a list of courses with ids that don't match the previous found. You can even make it as one composite SQL query.
Course.where.not(id: Course.ransack({user_assigned_content_skills_skill_name_cont: 'ruby'}).result)
This will generate an SQL like this:
SELECT courses.*
FROM courses
WHERE courses.id NOT IN (
SELECT courses.id FROM courses
LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user'
LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id
WHERE (skills.name ILIKE '%ruby%')
)

How to join tables to get only the last record of has-many association?

I have two models: Company and Transaction (has-many relationship). Transaction model has balance attribute. I have a query to join the models:
scope :joined_transactions, (lambda do
select('transactions.balance as current_balance')
.joins('LEFT OUTER JOIN transactions ON transactions.company_id = companies.id')
end)
However, I want to include only the last transaction into this query. As a result Company.joined_transactions.first.current_balance == Company.first.transactions.last.balance should be true.
subquery .joins('left join transactions t on t.company_id = companies.id AND t.id = (SELECT MAX(id) FROM transactions WHERE transactions.company_id = t.company_id)')
I believe this problem was already solved many times. You can do it using subquery or double join.
Please add more info on your current case if this doesn't work for you

How to write two inner join for Rails Active Record query

I want to write a rails active record query for this SQL query:
SELECT c.name FROM categories as c where c.id IN (select c.parent_id FROM categories as c join categories_coaches as cc on c.id=cc.category_id where cc.coach_id=17 group by c.parent_id)
Models:
class Category < ActiveRecord::Base
has_and_belongs_to_many :coaches
end
class Coach < ActiveRecord::Base
has_and_belongs_to_many :categories
end
Hey try in this way
categories = Category.where(:id => Category.joins(:coaches).where("coaches.id = ?", 17).group(:parent_id).map(&:parent_id))
Other solutions propose use of array operations, which is not a good idea.
You don't actually need grouping and subquerying. The same result can be obtained by rewriting your query to the following form:
SELECT categories.* FROM categories
INNER JOIN categories c2 ON c2.parent_id = categories.id
INNER JOIN categories_coaches cc ON c2.id = cc.category_id
WHERE cc.coach_id = 17
Which translates into ActiveRecord this way:
Category.joins('INNER JOIN categories c2 ON c2.parent_id=categories.id')
.joins('INNER JOIN categories_coaches cc ON c2.id = cc.category_id')
.where('cc.coach_id = ?', 17)
I'm not sure, but try this:
items = Category.joins(:coaches).where("caoches.coach_id = ?", 17).group(:parent_id).pluck(:parent_id)
categories = Category.where (id: items)

Left outer join query (i think)

I have two tables that look like this:
Products: id category name description active
Sales_sheets: id product_id link
product_id is a foreign key from the products id table
I wrote a prepared statement JOIN like this which works:
SELECT p.name, p.description, s.link FROM products AS p
INNER JOIN sales_sheets AS s ON p.id = s.product_id WHERE active=1 AND category=?
Basically a product can have a link to a PDF, but not every product will have a sales sheet. So if i try to bring up a product which doesn't have a sales sheet attached to it then it always returns no rows.
So i thought I'd have to use a LEFT OUTER JOIN in place of the INNER JOIN, but that returns no rows too, am I naming the tables in the wrong order? I've never had to use an OUTER join before?
SELECT p.name, p.description, s.link FROM products p
LEFT JOIN sales_sheets s ON p.id = s.product_id
WHERE active = 1 && category = ?

Resources