Use associated model scope - ruby-on-rails

If I have a model called User that has_many :games and games has_many :events- How can I get all the users which do not have any games OR have any games with a suspended event.
Here is what I have tried:
Example:
User class:
scope :with_no_games -> {
joins("LEFT OUTER JOIN games g on g.user_id = users.id").where("games.id IS NULL")
}
scope :with_suspended_games_or_no_games -> {
with_no_games.merge(Game.suspended) # <- this doesn't work
}
Game class:
scope :suspended -> {
joins("INNER JOIN events e ON event.game_id = games.id AND event.name = 'suspended'")
}
If I run User.with_suspended_games_or_no_games I get the following SQL statement:
SELECT \"users\".* FROM \"users\" LEFT OUTER JOIN games ON games.user_id = users.id INNER JOIN events ON events.game_id = games.id AND events.name = 'suspended' WHERE (games.user_id IS NULL)"
Which is not what I am looking for. What am I missing?

I think you are looking for
scope :with_suspended_games_or_no_games -> {
with_no_games.joins(:games).merge(Game.suspended)
}
to see the sql try running the following from a rails console:
User.with_suspended_games_or_no_games.to_sql
bonus
you could also write your scopes like:
User
scope :with_no_games -> { joins(:games).where(games: {id: nil}) }
scope :with_suspended_games_or_no_games -> { joins(:games).with_no_games.merge(Game.suspended) }
Game
scope :suspended -> { joins(:events).where(events: {name: 'suspended'}) }

Related

Scope Order by Count with Conditions Rails

I have a model Category that has_many Pendencies. I would like to create a scope that order the categories by the amount of Pendencies that has active = true without excluding active = false.
What I have so far is:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).order('COUNT(pendencies.id) DESC')}
This will order it by number of pendencies, but I want to order by pendencies that has active = true.
Another try was:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).where('pendencies.active = ?', true).order('COUNT(pendencies.id) DESC')}
This will order by number of pendencies that has pendencies.active = true, but will exclude the pendencies.active = false.
Thank you for your help.
I guess you want to sort by the amount of active pendencies without ignoring categories that have no active pendencies.
That would be something like:
scope :order_by_pendencies, -> {
active_count_q = Pendency.
group(:category_id).
where(active: true).
select(:category_id, "COUNT(*) AS count")
joins("LEFT JOIN (#{active_count_q.to_sql}) AS ac ON ac.category_id = id").
order("ac.count DESC")
}
The equivalent SQL query:
SELECT *, ac.count
FROM categories
LEFT JOIN (
SELECT category_id, COUNT(*) AS count
FROM pendencies
GROUP BY category_id
WHERE active = true
) AS ac ON ac.category_id = id
ORDER BY ac.count DESC
Note that if there are no active pendencies for a category, the count will be null and will be added to the end of the list.
A similar subquery could be added to sort additionally by the total amount of pendencies...
C# answer as requested:
method() {
....OrderBy((category) => category.Count(pendencies.Where((pendency) => pendency.Active))
}
Or in straight SQL:
SELECT category.id, ..., ActivePendnecies
FROM (SELECT category.id, ..., count(pendency) ActivePendnecies
FROM category
LEFT JOIN pendency ON category.id = pendency.id AND pendnecy.Active = 1
GROUP BY category.id, ...) P
ORDER BY ActivePendnecies;
We have to output ActivePendnecies in SQL even if the code will throw it out because otherwise the optimizer is within its rights to throw out the ORDER BY.
For now I developed the following (it's working, but I believe that it's not the best way):
scope :order_by_pendencies, -> { scoped = Category.left_joins(:pendencies)
.group(:id)
.order('COUNT(pendencies.id) DESC')
.where('pendencies.active = ?', true)
all = Category.all
(scoped + all).uniq}

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.

How to isolate a query inside a scope from pundit policy scope?

I'm using Rails 5 + Pundit gem and trying to fetch some chats with policy scope and model scope. Model scope has a query inside it and the problem is that policy scope applies to this inner query. The question is how to isolate the query from outer scope? Here's some code:
# model
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: User.find(user_id).chats.ids }) : all
}
# policy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.joins(:chat_users).where(chat_users: { user_id: user.id })
end
end
end
So I decided to output the inner sql query, which should get user chats' ids from the scope. I updated the model scope:
scope :with_user, ->(user_id=nil) {
puts User.find(user_id).chats.to_sql
where(chats: { id: User.unscoped.find(user_id).chats.ids } )
}
and here are results:
when I run ChatPolicy::Scope.new(User.first, Chat).resolve.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users"
"chat_users_chats" ON "chat_users_chats"."chat_id" = "chats"."id"
INNER JOIN "chat_users" ON "chats"."id" = "chat_users"."chat_id" WHERE
(chat_users.user_id = 350) AND "chat_users"."user_id" = 358
When I run Chat.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users" ON "chats"."id"
= "chat_users"."chat_id" WHERE "chat_users"."user_id" = 358
It generates the correct query if I run it without policy scope. Is there a workaround?
This is a Community Wiki answer replacing the answer having been edited into the original question as recommended by Meta.
This has been solved by the OP with a different unscoped model scope:
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: Chat.unscoped.joins(:chat_users).where(chat_users: { user_id: user_id }).ids } ) : all
}

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) }

Find record which doesn't have any associated records with a specific value

I have a couple of models: User and UserTags
A User has_many UserTags
A UserTag belongs_to User
I am trying to find all the Users which don't have a UserTag with name 'recorded' (so I also want users which don't have any tags at all). Given a users relation, I have this:
users.joins("LEFT OUTER JOIN user_tags ON user_tags.user_id = users.id AND user_tags.name = 'recorded'").
where(user_tags: { id: nil })
Is there any other better or more Railsy way of doing this?
Try this:
users.joins("LEFT OUTER JOIN user_tags ON user_tags.user_id=users.id").where("user_tags.name != ? OR user_tags.id is NULL", 'recorded')
This one should work:
users.joins(:user_tags).where.not(user_tags: { name: 'recorded' })
Joins not eager load nested model you should use "Includes or eage_load"
users.eager_load(:user_tags).where.not(user_tags: { name: 'recorded' })
This will use left outer join and you can update your query in where clause.
Same as
users.includes(:user_tags).where.not(user_tags: { name: 'recorded' })
Try this, It will return the users with 0 user_tags :
users = users.joins(:user_tag).where("users.id IN (?) OR user_tags.name != ?",User.joins(:user_tag).group("users.id").having('count("user_tag.user_id") = 0'), "recorded")
Hey you can simply use includes for outer join as user_tags.id is null returns all your record not having user_tags and user_tags.name != 'recorded' returns record having user_tag name is not recorded
users.includes(:user_tags).where("user_tags.id is null or user_tags.name != ?","recorded")
Or you can also used using not in clause as but it is not optimised way for given query:
users.includes(:user_tags).where("users.id not in (select user_id from user_tags) or user_tags.name != ?","recorded")

Resources