Rails query condition or relation - ruby-on-rails

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]]

Related

How to find a record through an association?

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.

Merge ActiveRecord_Relation

In model Banner
belongs_to :segment
belongs_to :basic_component
has_many :state_banners, dependent: :destroy
has_many :states, through: :state_banners
scope :banner_have_zero_cities, lambda { includes(state_banners: :state_banner_cities).where(state_banner_cities: {state_banner_id: nil}) }
scope :banner_by_state, lambda { |state_id| where("state_banners.state_id = ?", state_id) }
scope :banner_by_city, lambda { |city_id| joins(state_banners: :state_banner_cities).where("state_banner_cities.city_id = ?", city_id) }
In controller
def scoped_collection
#banners_cities = Banner.banner_by_city(city_id)
#banners_states =Banner.banner_by_state(city.state_id).banner_have_zero_cities
#banners = #banners_cities.concat(#banners_states)
return #banners.joins(:basic_component)
end
#banners_states.size
=> 1
#banners_cities.size
=> 2
#banners_states.merge(#banners_cities)
SQL (0.2ms) SELECT DISTINCT banners.id FROM banners INNER JOIN state_banners ON state_banners.banner_id = banners.id INNER JOIN state_banner_cities ON state_banner_cities.state_banner_id = state_banners.id WHERE (state_banners.state_id = 3) AND state_banner_cities.state_banner_id IS NULL AND (state_banner_cities.city_id = '260') LIMIT 25 OFFSET 0
=> []
I need 3
i try concat
#banners = #banners_cities.concat(#banners_states)
#banners.size => 3
but
#banners.joins(:basic_component).order("basic_component.order asc").size => 2
CACHE (0.0ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM banners INNER JOIN state_banners ON state_banners.banner_id = banners.id INNER JOIN state_banner_cities ON state_banner_cities.state_banner_id = state_banners.id INNER JOIN basic_components ON basic_components.id = banners.basic_component_id WHERE (state_banner_cities.city_id = '260') LIMIT 25 OFFSET 0) subquery_for_count
:(, help
Your post is kind of hard to follow, but try .limit(3) at the end of the query?

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

Change multiple counters in the same query with counter_culture

I have the following class with several counter_culture counters for the same association:
class Book < ::ActiveRecord::Base
belongs_to :user
counter_culture :user
counter_culture :user, column_name: Proc.new { |b| b.comedy? ? 'comedy_books_count' : nil }
counter_culture :user, column_name: Proc.new { |b| b.hard_cover? ? 'hard_cover_books_count' : nil }
end
The issue I have with this is that it executes the updates using several queries:
UPDATE "users" SET "books_count" = COALESCE("books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
UPDATE "users" SET "comedy_books_count" = COALESCE("comedy_books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
UPDATE "users" SET "hard_cover_books_count" = COALESCE("hard_cover_books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
Is there a way to do this more efficiently with counter_culture by executing the updates in a single query?
Each line is detected as a special case, it would be difficult for them to know which one they can optimise and which one they cannot. So I guess no.

Resources