Merge ActiveRecord_Relation - ruby-on-rails

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?

Related

Rails query where at least 1 of the associated records contains an attribute

I have a GroupMeetings table and a GroupMeetingsUser table which is a join table between User and GroupMeetings. I want to find all the GroupMeetings where at least 1 of the GroupMeetingsUser has an attribute.
Right now, this works:
#group_meetings = GroupMeeting.where('lang_one_id IN (?) AND lang_two_id IN (?) AND meeting_time >= ?', #user.languages.pluck(:id), #user.languages.pluck(:id), Date.today)
#new_group_meetings_id = []
#group_meetings.each do |meeting|
meeting.group_meetings_user.each do |user|
if(user.user.location === #user.location)
#new_group_meetings_id.push(meeting.id)
end
end
end
#group_meetings = GroupMeeting.where('id IN (?)', #new_group_meetings_id)
But how can I include the .each loop in original GroupMeetings query instead? Like using .joins(:group_meetings_user) to find all the records where at least 1 of the users has an attribute?
class GroupMeeting < ApplicationRecord
has_many :group_meetings_users
has_many :users, through: :group_meetings_users
end
class GroupMeetingsUser < ApplicationRecord
belongs_to :user
belongs_to :group_meeting
validates_presence_of :user_id, :group_meeting_id
validates :user_id, :uniqueness => {:scope => :group_meeting_id, :message => 'can only join each group once.'}
end
UPDATE 1:
GroupMeeting.joins(:users).where(group_meeting: { lang_one_id: #user.languages.pluck(:id), lang_two_id: #user.languages.pluck(:id), meeting_time: DateTime.now..DateTime::Infinity.new}, user: { location: #user.location })
gives the error:
no such column: group_meeting.lang_one_id: SELECT "group_meetings".* FROM "group_meetings" INNER JOIN "group_meetings_users" ON "group_meetings_users"."group_meeting_id" = "group_meetings"."id" INNER JOIN "users" ON "users"."id" = "group_meetings_users"."user_id" WHERE "group_meeting"."lang_one_id" IN (29, 30, 31, 22) AND "group_meeting"."lang_two_id" IN (29, 30, 31, 22) AND ("group_meeting"."meeting_time" >= ?) AND "user"."location" = ?
I suggest the following:
GroupMeeting
.joins('INNER JOIN languages l1 ON languages l1.id = group_meetings.lang_one_id')
.joins('INNER JOIN languages l2 ON languages l2.id = group_meetings.lang_two_id')
.joins(:group_meeting_users => :users)
.where('meeting_time >= ?', Date.today)
.select('group_meetings.*')
.group('group_meetings.id')
.having('users.location = ?', #user.location)
It should be faster than doing sub-selects (using languages.id IN (?) and pluck)
Let me know if that works and/or if you have questions.

Combining scopes doesn't work

I'm trying to combine three scopes in one (one scope uses the other two).
I want to get all videos which don't have certain categories and certain tags.
Video
class Video < ActiveRecord::Base
self.primary_key = "id"
has_and_belongs_to_many :categories
has_and_belongs_to_many :tags
scope :with_categories, ->(ids) { joins(:categories).where(categories: {id: ids}) }
scope :excluded_tags, -> { joins(:tags).where(tags: {id: 15}) }
scope :without_categories, ->(ids) { where.not(id: excluded_tags.with_categories(ids) ) }
end
But when I call
#excluded_categories = [15,17,26,32,35,36,37]
#videos = Video.without_categories(#excluded_categories)
I still get video which has tag 15.
The SQL query which server is firing looks like this
SELECT "videos"."video_id" FROM "videos" WHERE ("videos"."id" NOT IN (SELECT "videos"."id" FROM "videos" INNER JOIN "tags_videos" ON "tags_videos"."video_id" = "videos"."id" INNER JOIN "tags" ON "tags"."id" = "tags_videos"."tag_id" INNER JOIN "categories_videos" ON "categories_videos"."video_id" = "videos"."id" INNER JOIN "categories" ON "categories"."id" = "categories_videos"."category_id" WHERE "tags"."id" = $1 AND "categories"."id" IN (15, 17, 26, 32, 35, 36, 37))) [["id", 15]]
Am I doing something wrong?
I think that you must use one scope for excluding categories and a second one for excluding tags and then combine them.
scope :without_categories, ->(ids) { joins(:categories).where.not(categories: {id: ids}) }
scope :without_tags, ->(ids) { joins(:tags).where.not(tags: {id: ids}) }
Then you can use
#excluded_categories = [1,2,3,4]
#excluded_tags = [1,2,3,4,5,6]
#videos = Video.without_categories(#excluded_categories).without_tags(#excluded_tags)
EDIT after comment
After seen the query because chained scopes are using AND the produced query cannot return the desired result. A new approach would be to create one scope only for this purpose.
scope :without_categories_tags, ->
(category_ids, tag_ids) { joins( :categories, :tags).
where('categories.id NOT IN (?) OR tags.id NOT IN (?)', category_ids, tag_ids)}
The you can use
#excluded_categories = [1,2,3,4]
#excluded_tags = [1,2,3,4,5,6]
#videos = Video.without_categories_tags(#excluded_categories,#excluded_tags)
scope :excluded_tags, -> { joins(:tags).where.not(tags: {id: 15}) }
scope :without_categories, ->(ids) { excluded_tags.with_categories(ids) }

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

ActiveRecord select add count

In my ActiveRecord query, I need to provide this info in the select method:
(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count,
(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count
Of course, I pass pass these two as string to the .select() part, but I am wondering what's the proper/alternative way of doing this?
Here's the complete query I am trying to call:
SELECT DISTINCT
spentits.*,
username,
(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count,
(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count,
(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_count,
(case when likes.id is null then 0 else 1 end) as is_liked_by_me,
(case when wishlist_items.id is null then 0 else 1 end) as is_wishlisted_by_me,
(case when comments.id is null then 0 else 1 end) as is_commented_by_me
FROM spentits
LEFT JOIN users ON users.id = spentits.user_id
LEFT JOIN likes ON likes.user_id = 9 AND likes.spentit_id = spentits.id
LEFT JOIN wishlist_items ON wishlist_items.user_id = 9 AND wishlist_items.spentit_id = spentits.id
LEFT JOIN comments ON comments.user_id = 9 AND comments.spentit_id = spentits.id
WHERE spentits.user_id IN
(SELECT follows.following_id
FROM follows
WHERE follows.follower_id = 9 AND follows.accepted = 1)
ORDER BY id DESC LIMIT 15 OFFSET 0;
All the tables here have their respective ActiveRecord object. Just really confused how to convert this query into 'activerecord'/rails way with writing least amount of SQL. The '9' user_id is suppose to be a parameter.
Update:
Ok so here's what I did inmean time, it's much better than raw SQL statement, but it still looks ugly to me:
class Spentit < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :wishlist_items
has_many :comments
scope :include_author_info, lambda {
joins([:user]).
select("username").
select("users.photo_uri as user_photo_uri").
select("spentits.*")
}
scope :include_counts, lambda {
select("(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count").
select("(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count").
select("(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_items_count").
select("spentits.*")
}
end
Using these scope methods, I can do:
Spentit.where(:id => 7520).include_counts.include_author_info.customize_for_user(45)
A bit about the classes. A User has many Spentits. A Spentit has many comments, likes and comments.
Ok, you're "doing it wrong", a little bit. Rather than
scope :include_counts, lambda {
select("(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count").
select("(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count").
select("(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_items_count").
select("spentits.*")
}
do
Spentit.find(7520).likes.count
Spentit.find(7520).wishlist_items.count
Spentit.find(7520).comments.count
Instead of
scope :include_author_info, lambda {
joins([:user]).
select("username").
select("users.photo_uri as user_photo_uri").
select("spentits.*")
}
do
Spentit.find(7520).user.username
Spentit.find(7520).user.photo_uri
Also, you can define scopes within the referenced models, and use those:
class Follow < ActiveRecord::Base
belongs_to :follower, :class_name => "User"
belongs_to :following, :class_name => "User"
scope :accepted, lambda{ where(:accepted => 1) }
end
Spentits.where(:user => Follow.where(:follower => User.find(9)).accepted)
Now, maybe you also do:
class Spentit
def to_hash
hash = self.attributes
hash[:like_count] = self.like.count
# ...
end
end
but you don't need to do anything fancy to get those counts "under normal circumstances", you already have them.
Note, however, you'll probably also want to do eager loading, which you can apparently make as part of the default scope, or you'll do a lot more queries than you need.

Resources