Scope Order by Count with Conditions Rails - ruby-on-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}

Related

How to do a query with mulitple joins in AR

My simplified database :
For example, I would like to get all pois with way_id = 2 (through track_ways & poi_tracks)
I have a scope in my way model :
scope :by_way, ->(way_id) { joins(:ways).where('ways.id = ?', way_id) }
I'm using this scope in my query :
Poi.joins(:tracks).where(tracks: Track.by_way(2))
But the result is not the expected one
Spoiler:
Poi.joins(poi_tracks: [track: [:track_ways, :ways]]).where('ways.id = ?', 2)
First join with poi_tracks
Poi.joins(:poi_tracks).all
Join with tracks
Poi.joins(poi_tracks: [:track]).all
Join with track_ways
Poi.joins(poi_tracks: [track: [:track_ways]]).all
Join with ways
Poi.joins(poi_tracks: [track: [:track_ways, :ways]])
Apply way condition
Poi.joins(poi_tracks: [track: [:track_ways, :ways]]).where('ways.id = ?', 2)

Scope model values and count in a single query | Rails 5

I have models as follows:
class QuestionResult
belongs_to :test_result
belongs_to :test
scope :answered -> { where("answered = ?",true) }
scope :unanswered, -> { where("answered IS NULL OR answered != ?",true) }
end
class TestResult
belongs_to :test
has_many :question_results
end
Now I need these values:
test_result.question_results.answered.count
test_result.question_results.unanswered.count
test_result.review_questions.count
review_questions is a column in test_results table.
So I need the value of count of review_questions in the test_results table and values of the counts of scopes: answered and unanswered in the associated question_results table.
There is no column unanswered in the question_results table. It is only a scope but there is a column with the name answered.
At present, I am querying these values as shown above.
Is there any way that I can do the same in a single query?
Update 1
I am able to combine the first two queries into a single query without using scopes in query as below:
test_result.question_results.group(:answered).count
How can I combine test_result.review_questions.count query into the updated query?
Update 2
SELECT
test_results.id,
sum(if (question_results.answered = 1, 1, 0)) as answered,
sum(if (question_results.answered = 0 or question_results.answered is null, 1, 0)) as unanswered,
test_results.review_questions
FROM test_results
INNER JOIN question_results ON question_results.section_result_id = test_results.id
WHERE test_results.id = test.test_result.last.id;
This is the working sql query. What is its equivalent rails activerecord query?
I don't know what you mean with _value of count of review_questions in the test_results_. If it's a column in the table itself why are you going to count it?
About the answered and unanswered scopes, without CTE you can use sub-queries:
SELECT
test_results.id,
(SELECT COUNT(*)
FROM question_results q
WHERE q.answered = true
AND q.test_result_id = test_results.id) AS total_answered,
(SELECT COUNT(*)
FROM question_results q
WHERE q.answered = false
OR q.answered IS NULL
AND q.test_result_id = test_results.id) AS total_unanswered
FROM test_results
WHERE test_results.id = id
The further you can go using Rails with this is storing each sub-select and pass it within the ActiveRecord select method:
answered = '(SELECT COUNT(*) FROM question_results q WHERE q.answered = true AND q.test_result_id = test_results.id) AS answered'
unanswered = '(SELECT COUNT(*) FROM question_results q WHERE q.answered = false OR q.answered IS NULL AND q.test_result_id = test_results.id) AS unanswered'
TestResult.select(:id, answered, unanswered).where(id: id).as_json
As seen in the discussion, you can just "convert" the raw SQL you're using with ActiveRecord methods (sadly, not completely):
TestResult
.select(
:id,
:review_questions,
'SUM(IF(question_results.answered = true, 1, 0)) AS answered',
'SUM(IF(question_results.answered = true OR question_results.answered IS NULL, 1, 0)) AS unanswered')
.joins(:question_results)
.group(:id)
.as_json
This is one option.
answered_count = test_result.question_results.answered.count
unanswered_count = TestResult.count - answered_count
Also, I'd recommend removing the belongs_to :test on QuestionResult. TestResult already has a foreign key reference to :test, so the QuestionResult's association with both the :test and the :test_result is redundant.
Or,
belongs_to :test, :through => :test_result
can get rid of the redundant foreign key as well

How to efficiently retrieve groups of objects from activerecord with a single SQL query?

I have a table products which has a product_type_code column on it. What I'd like to do is retrieve different numbers of objects based on this column (eg.: 3 products with product_type_code = 'fridge', 6 products with product_type_code = 'car', 9 products with product_type_code = 'house', etc.).
I know I can do like this:
fridges = Product.where(product_type_code: 'fridge').limit(3)
houses = Product.where(product_type_code: 'house').limit(9)
[...]
And even create a scope like this:
# app/models/product.rb
scope :by_product_type_code, -> (material) { where(product_type_code: product_type_code) }
However, this is not efficient since I go to the database 3 times, if I'm not wrong. What I'd like to do is something like:
scope :by_product_type_code, -> (hash) { some_method(hash) }
where hash is: { fridge: 3, car: 6, house: 9 }
and get an ActiveRecord_Relation containing 3 fridges, 6 cars and 9 houses.
How can I do that efficiently?
You can create a query using UNION ALL, which selects records having a specifc product_type_code and limit to use it with find_by_sql:
{ fridge: 3, car: 6, house: 9 }.map do |product_type_code, limit|
"(SELECT *
FROM products
WHERE product_type_code = '#{product_type_code}'
LIMIT #{limit})"
end.join(' UNION ALL ')
And you're gonna have a query like:
(SELECT * FROM products WHERE product_type_code = 'fridge'LIMIT 3)
UNION ALL
(SELECT * FROM products WHERE product_type_code = 'car'LIMIT 6)
UNION ALL
(SELECT * FROM products WHERE product_type_code = 'house'LIMIT 9)
#SebastianPalma's answer is the best solution; however if you were looking for a more "railsy" fashion of generating this query you can use arel as follows:
scope :by_product_type_code, ->(h) {
products_table = self.arel_table
query = h.map do |product_type,limit|
products_table.project(:id)
.where(products_table[:product_type_code].eq(product_type))
.take(limit)
end.reduce do |scope1, scope2|
Arel::Nodes::UnionAll.new(scope1,scope2)
end
self.where(id: query)
end
This will result in the sub query being part of the where clause.
Or
scope :by_product_type_code, ->(h) {
products_table = self.arel_table
query = h.map do |product_type,limit|
products_table.project(Arel.star)
.where(products_table[:product_type_code].eq(product_type))
.take(limit)
end.reduce do |scope1, scope2|
Arel::Nodes::UnionAll.new(scope1,scope2)
end
sub_query = Arel::Nodes::As.new(query,products_table)
self.from(sub_query)
end
This will result in the subquery being the source of the data.

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

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