Scope model values and count in a single query | Rails 5 - ruby-on-rails

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

Related

ActiveRecord::AssociationRelation loses foreign key value when combined with a UNION

I'm trying to write a named scope that uses a union:
# Simplified example
class MyModel < ActiveRecord::Base
belongs_to :owner
scope :my_named_scope, -> {
scope_a = where("#{arel_table.name}.a = 'foo'")
scope_b = where("#{arel_table.name}.b = 'bar'")
union = scope_a.union(scope_b)
from(union)
}
end
MyModel.my_named_scope works fine
owner.my_models.my_named_scope does not
In the failing case, I can call .to_sql on the ActiveRecord::AssociationRelation which my_named_scope returns, and that SQL has a syntax error:
SELECT "my_models".* FROM ( SELECT "my_models".* FROM "my_models" WHERE "my_models"."owner_id" = 1001 AND (my_models.a = 'foo') UNION SELECT "my_models".* FROM "my_models" WHERE "my_models"."owner_id" = AND (my_models.b = 'bar') ) WHERE "my_models"."owner_id" =
...The sql just cuts off after "owner_id" = as if it can't find the owner_id, even though the owner_id (1001) appears earlier in the query. How do I correct this?
TMI: Rails 4.2 (I know it's old. I'm at a big company with big tech debt.)

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}

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.

How can I write the most efficient scope for asking only those parent objects which have some certain child objects

I have:
class Evaluation < ActiveRecord::Base
has_many :scores
end
class Score < ActiveRecord::Base
scope :for_disability_and_score,
lambda { |disability, score|
where('total_score >= ? AND name = ?', score, disability)
}
end
The scores table has a total_score field and a name field.
How can i write a scope to ask for only those evaluations which have a Score with name 'vision' and total_score of 2 and they have another Score with name 'hearing' and total_score of 3.
And how can all this be generalized to ask for those evaluations which have n scores with my parameters?
In raw sql it would be like:
I managed to do it in raw sql:
sql = %q-SELECT "evaluations".*
FROM "evaluations"
INNER JOIN "scores" AS s1 ON "s1"."evaluation_id" = "evaluations"."id"
INNER JOIN "target_disabilities" AS t1 ON "t1"."id" = "s1"."target_disability_id"
INNER JOIN "scores" AS s2 ON "s2"."evaluation_id" = "evaluations"."id"
INNER JOIN "target_disabilities" AS t2 ON "t2"."id" = "s2"."target_disability_id"
WHERE "t1"."name" = 'vision' AND (s1.total_score >= 1)
AND "t2"."name" = 'hearing' AND (s2.total_score >= 2)-
Here the point is to repeat this:
INNER JOIN "scores" AS s1 ON "s1"."evaluation_id" = "evaluations"."id"
and this (by replacing s1 to s2 and s3 and so forth):
WHERE (s1.total_score >= 1)
But it should be a rails way of doing this... :) Hopefully
Try this:
scope :by_scores, lambda do |params|
if params.is_a? Hash
query = joins(:scores)
params.each_pair do |name, score|
query = query.where( 'scores.total_score >= ? AND scores.name = ?', score, name)
end
query
end
end
Then call like this:
Evaluation.by_scores 'vision' => 2, 'hearing' => 3

Resources