How to find a record through an association? - ruby-on-rails

There are four tables - chat_rooms, chat_messages, chat_rooms_and_users and users:
chat_rooms - The rooms have messages and users.
chat_rooms_and_users - Through this table users are connected to rooms.
Each room can have two users.
How to find a room knowing two users?
I tried like this:
room = joins(:chat_rooms_and_users)
.find_by(
type: ChatRoom.types[:private],
chat_rooms_and_users: {
user: [user_a, user_b]
}
)
SELECT "chat_rooms".* FROM "chat_rooms" INNER JOIN "chat_rooms_and_users" ON "chat_rooms_and_users"."room_id" = "chat_rooms"."id" WHERE "chat_rooms"."type" = $1 AND "chat_rooms_and_users"."user_id" IN ($2, $3) LIMIT $4 [["type", 0], ["user_id", 497], ["user_id", 494], ["LIMIT", 1]]
This bothers me in the SQL code:
"chat_rooms_and_users"."room_id" = "chat_rooms"."id"
If there are no rooms, then the first room is created normally. But then there is always only the room whose ID is first than the rest.

Your issue is "chat_rooms_and_users"."user_id" IN ($2, $3) will return all "private" rooms where either user is present.
Instead you want to find a chatroom where both are present.
I would recommend making a scope for this
#assumed
class User < ApplicationRecord
has_many :chat_rooms_and_users
end
class ChatRoom < ApplicationRecord
scope :private_by_users, ->(user_a,user_b) {
where(type: ChatRoom.types[:private])
.where(id: user_a.chat_rooms_and_users.select(:chat_room_id))
.where(id: user_b.chat_rooms_and_users.select(:chat_room_id))
}
end
#Then
ChatRoom.private_by_users(user_a,user_b)
This will return a collection of "private" rooms where user_a and user_b are both participants. The SQL will look akin to:
SELECT "chat_rooms".*
FROM "chat_rooms"
WHERE "chat_rooms"."type" = 0 AND
"chat_rooms"."id" IN (
SELECT
"chat_rooms_and_users"."chat_room_id"
FROM
"chat_rooms_and_users"
WHERE
"chat_rooms_and_users"."user_id" = user_a_id
) AND "chat_rooms"."id" IN (
SELECT
"chat_rooms_and_users"."chat_room_id"
FROM
"chat_rooms_and_users"
WHERE
"chat_rooms_and_users"."user_id" = user_b_id
)
If you can guarantee there will only ever be 1 or 0 rooms, of this type, with both participants then you can add first to the end of this chain.

Related

Rails ActiveRecord includes with condition for the second query

I have three models: Sale, Product and Client. A sale belongs to both a product and a client, each of which has many sales. Client also has many products through sales (basically products which have ever been sold to that client). The query for client.products is
SELECT "products".* FROM "products" INNER JOIN "sales" ON "products"."id" = "sales"."product_id" WHERE "sales"."client_id" = $1
So far, so good. Now I want to eager load sales for products which belong to a client (client.products.includes(:sales)), which correctly fetches all sales for each product for this client, like this:
SELECT "products".* FROM "products" INNER JOIN "sales" ON "products"."id" = "sales"."product_id" WHERE "sales"."client_id" = $1;
SELECT "sales".* FROM "sales" WHERE "sales"."product_id" IN (...)
The problem is I would like to only fetch sales which belong to that client. The query should look like this:
SELECT "products".* FROM "products" INNER JOIN "sales" ON "products"."id" = "sales"."product_id" WHERE "sales"."client_id" = $1;
SELECT "sales".* FROM "sales" WHERE "sales"."client_id" = $1 AND "sales"."product_id" IN (...)
I've tried doing
client.products.includes(:sales).where(sales: { client: client } })
and
client.products.includes(:sales).where(products: { sales: { client: client } })
but unfortunately it only alters the first query like this:
WHERE "sales"."client_id" = $1 AND "sales"."client_id" = $2
Both $1 and $2 are the same value.
Is what I'm trying to do even possible with includes? If so how do I do it?
sales = client.sales.includes(:product)
This will return you all the sales and eager load products for a sale/client.
To get the products you can loop through the sales
sales.each do |sale|
sale.product
end
Edit
You can group sales by products
products_with_sales = sales.group_by(&:product)
this will return you an array of hashes
key = product
value = [sales]
then you can perform you calculations
products_with_sales.each do |product, sales|
# sales.map { ... }
# sales.sum(&:method)
end

How can I make a outer join in a complex Rails has_many scope?

My goal is to make a scope where I can see all the completed courses of a Member object.
A Course is composed of many Sections. There is one Quiz per Section. And every single Quiz must be set to complete in order for the Course to be complete.
For example :
Course
Section A -> Quiz A ( Complete )
Section B -> Quiz B ( Complete )
Section C -> Quiz C ( Complete )
This is my best attempt at writing this kind of scope :
# Member.rb
has_many :completed_courses, -> {
joins(:quizzes, :sections)
.where(quizzes: {completed: true})
.group('members.id HAVING count(sections.quizzes.id) = count(sections.id)')
}, through: :course_members, source: :course
The part that I'm missing is this part count(sections.quizzes.id) which isn't actually SQL. I'm not entirely sure what kind of JOIN this would be called, but I need some way to count the completed quizzes that belong to the course and compare that number to how many sections. If they are equal, that means all the quizzes have been completed.
To be fair, just knowing the name of this kind of JOIN would probably set me in the right direction.
Update
I tried using #jamesdevar 's response :
has_many :completed_courses, -> {
joins(:sections)
.joins('LEFT JOIN quizzes ON quizzes.section_id = sections.id')
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
.group('courses.id')
}, through: :course_members, source: :course
But it returns a [ ] value when it shouldn't. For example I have this data :
-> Course{id: 1}
-> Section{id: 1, course_id: 1}
-> Quiz{section_id: 1, member_id: 1, completed: true}
The Course has one Section in total. The Section may have hundreds of Quizzes associated with it from other Members, but for this specific Member, his Quiz is completed.
I think it has something to do with this SQL comparison not being unique to the individual member.
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
The actual SQL produced is this :
SELECT "courses".* FROM "courses"
INNER JOIN "sections" ON "sections"."course_id" = "courses"."id"
INNER JOIN "course_members" ON "courses"."id" = "course_members"."course_id"
LEFT JOIN quizzes ON quizzes.section_id = sections.id
WHERE "course_members"."member_id" = $1
GROUP BY courses.id
HAVING COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)
ORDER BY "courses"."title"
ASC [["member_id", 1121230]]
has_many :completed_courses, -> {
joins(:sections)
.joins('LEFT JOIN quizzes ON quizzes.section_id = sections.id')
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
.group('courses.id')
}, through: :course_members, source: :course
In this case having will filter each cources.id group by condition when each section has completed quizz.

Rails query condition or relation

It's my DB scheme:
branches
-----------------------------------
id
name:string
active:boolean
..
course_contents
-----------------------------------
id
title:string
show_all_branches:boolean
active:boolean
..
branches_course_contents
-----------------------------
branch_id
course_content_id
And my model files:
class Branch < ApplicationRecord
has_and_belongs_to_many :course_contents
scope :active, -> { where(active: true) }
end
class CourseContent < ApplicationRecord
has_and_belongs_to_many :branches
scope :active, -> { where(active: true) }
scope :show_all_branches, -> { where(show_all_branches: true) }
end
I'm trying to CourseContent.show_all_branches.merge(-> { joins(:branches) }) this command. It returns show_all_branches selected and has relations with branches table. But i need show_all_branches selected or has relations with branches table.
Thanks for helps. I solved this problem and I writing my solution for other peoples. I said actually need to branch's course_contents with show_all_branches selected in the course_contents table. I tried merge method with this CourseContent.show_all_branches.merge(-> { joins(:branches) }) or other many methods with other conditions. Cuz I used to joins/includes method all of my conditions. It's never returns true result for me. And I finally tried left_joins method and that solved my problem here.
My commands here:
#course.course_contents.active.left_joins(:branches).where("course_contents.show_all_branches = ? OR branches_course_contents.branch_id = ?", true, #branch.id)
Its sql result
SELECT "course_contents".* FROM "course_contents" LEFT OUTER JOIN "branches_course_contents" ON "branches_course_contents"."course_content_id" = "course_contents"."id" LEFT OUTER JOIN "branches" ON "branches"."id" = "branches_course_contents"."branch_id" WHERE "course_contents"."course_id" = 3 AND "course_contents"."active" = 't' AND (course_contents.show_all_branches = 't' OR branches_course_contents.branch_id = 1)
and its explain:
CACHE (0.0ms) SELECT "course_contents".* FROM "course_contents" LEFT OUTER JOIN "branches_course_contents" ON "branches_course_contents"."course_content_id" = "course_contents"."id" LEFT OUTER JOIN "branches" ON "branches"."id" = "branches_course_contents"."branch_id" WHERE "course_contents"."course_id" = $1 AND "course_contents"."active" = $2 AND (course_contents.show_all_branches = 't' OR branches_course_contents.branch_id = 1) [["course_id", 3], ["active", true]]
Branch Load (1424.1ms) SELECT "branches".* FROM "branches" WHERE "branches"."active" = $1 AND "branches"."slug" = $2 LIMIT $3 [["active", true], ["slug", "izmir"], ["LIMIT", 1]]

Change a instance method to a has_many on Rails

has_many.rb
has_many :child_attendances, -> (attendance) {
includes(:activity).references(:activity).
where(activities: {parent_activity_id: attendance.activity_id}).
where(
Attendance.arel_table.grouping(
Attendance.arel_table[:attendant_id].eq(attendance.attendant_id).
and(Attendance.arel_table[:attendant_type].eq(attendance.attendant_type))
).
or(Attendance.arel_table[:tag_code].eq(attendance.tag_code))
)
}, class_name: 'Attendance', dependent: :destroy
methods.rb
def self.child_attendances(attendance)
includes(:activity).references(:activity).
where(activities: {parent_activity_id: attendance.activity_id}).
where(
Attendance.arel_table.grouping(
Attendance.arel_table[:attendant_id].eq(attendance.attendant_id).
and(Attendance.arel_table[:attendant_type].eq(attendance.attendant_type))
).
or(Attendance.arel_table[:tag_code].eq(attendance.tag_code))
)
end
def child_attendances
self.class.child_attendances(self)
end
When using has_many.rb
-- attendance.child_attendances.to_sql
SELECT "attendances"."id" AS t0_r0 FROM "attendances" LEFT OUTER JOIN "activities" ON "activities"."id" = "attendances"."activity_id" WHERE "activities"."parent_activity_id" = 654 AND (("attendances"."attendant_id" IS NULL AND "attendances"."attendant_type" IS NULL) OR "attendances"."tag_code" = '123456789') AND "attendances"."attendance_id" = 164513
It appends "attendances"."attendance_id" = 164513, and attendance_id is not a valid column for Attendance.
When using methods.rb
-- attendance.child_attendances.to_sql
SELECT "attendances"."id" AS t0_r0 FROM "attendances" LEFT OUTER JOIN "activities" ON "activities"."id" = "attendances"."activity_id" WHERE "activities"."parent_activity_id" = $1 AND (("attendances"."attendant_id" IS NULL AND "attendances"."attendant_type" IS NULL) OR "attendances"."tag_code" = '123456789') [["parent_activity_id", 654]]
How to make the other snippet into a has_many? Rails seems to always look for a primary_key and foreign_key to make the join, when using has_many.
According to https://stackoverflow.com/a/34444220 what I was trying to do does not make sense to be built using has_many, so I'm sticking with an instance method.
def child_attendances
self.class.includes(:activity).references(:activity).
where(activities: {parent_activity_id: activity_id}).
where(
Attendance.arel_table.grouping(
Attendance.arel_table[:attendant_id].eq(attendant_id).
and(Attendance.arel_table[:attendant_type].eq(attendant_type))
).
or(Attendance.arel_table[:tag_code].eq(tag_code))
)
end

Rails | Add condition in join query

Is it possible, to add condition in join query?
For example I want to build next query:
select * from clients
left join comments on comments.client_id = clients.id and comments.type = 1
Client.joins(:comments).all generates only:
select * from clients
left join comments on comments.client_id = clients.id
PS. Client.joins("LEFT JOIN comments on comments.client_id = clients.id and comment.type = 1") isn't nice.
You can do:
Client.left_outer_joins(:comments)
.where(comments: { id: nil })
.or(Client.left_outer_joins(:comments)
.where.not(comments: { id: nil })
.where(comments: { type: 1 }))
what gives you something equivalent to what you want:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id"
WHERE "comments"."id" IS NULL
OR ("comments"."id" IS NOT NULL) AND "comments"."type" = 1
UPDATE
Actually, this does not work because rails close the parenthesis leaving outside the evaluation of type.
UPDATE 2
If yours comment types are few and is not probable its values will change, you can solve it with this way:
class Client < ApplicationRecord
has_many :comments
has_many :new_comments, -> { where comments: { type: 1 } }, class_name: Comment
has_many :spam_comments, -> { where comments: { type: 2 } }, class_name: Comment
end
class Comment < ApplicationRecord
belongs_to :client
end
With this new relationships in your model now you can do:
Client.left_joins(:comments)
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id"
Client.left_joins(:new_comments)
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
/*[["type", 1]]*/
Client.left_joins(:spam_comments)
gives the same query:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
/*[["type", 2]]*/
Client.left_joins(:new_comments).where name: 'Luis'
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
WHERE "clients"."name" = ?
/*[["type", 1], ["name", "Luis"]]*/
NOTE:
I try to use a parameter in the original has_many relationship like...
has_many :comments, -> (t) { where comments: { type: t } }
but this give me an error:
ArgumentError: The association scope 'comments' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
I ended up doing this:
meals_select = Meal.left_outer_joins(:dish_quantities).arel
on_expression = meals_select.source.right.first.right.expr
on_expression.children.push(DishQuantity.arel_table[:date].eq(delivery_date))
on_expression.children.push(DishQuantity.arel_table[:dish_id].eq(Dish.arel_table[:id]))
ON "dish_quantities"."meal_id" = "meals"."id" AND "dish_quantities"."date" = '2022-08-05' AND "dish_quantities"."dish_id" = "dishes"."id"
Client.joins(:comments).where(comments: {type: 1})
Try this

Resources