Reputation: 8734
I am working on an app that allows Members to take a survey (Member has a one to many relationship with Response). Response holds the member_id, question_id, and their answer.
The survey is submitted all or nothing, so if there are any records in the Response table for that Member they have completed the survey.
My question is, how do I re-write the query below so that it actually works? In SQL this would be a prime candidate for the EXISTS keyword.
def surveys_completed
members.where(responses: !nil ).count
end
Upvotes: 60
Views: 48276
Reputation: 7210
Since Rails 7 you can use where.associated
:
Before:
@account.users.joins(:contact).where.not(contact_id: nil)
After:
@account.users.where.associated(:contact)
Upvotes: 0
Reputation: 9238
Rails 6.1 introduces a new way to check for the absence of an association - where.missing.
Please, have a look at the following code snippet:
# Before:
Post.left_joins(:author).where(authors: { id: nil })
# After:
Post.where.missing(:author)
And this is an example of SQL query that is used under the hood:
Post.where.missing(:author)
# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE "authors"."id" IS NULL
As a result, your particular case can be rewritten as follows:
def surveys_completed
members.where.missing(:response).count
end
Thanks.
Sources:
Notes:
Upvotes: 7
Reputation: 54882
You can use includes
and then test if the related response(s) exists like this:
def surveys_completed
members.includes(:responses).where('responses.id IS NOT NULL')
end
Here is an alternative, with joins
:
def surveys_completed
members.joins(:responses)
end
The solution using Rails 4:
def surveys_completed
members.includes(:responses).where.not(responses: { id: nil })
end
Alternative solution using activerecord_where_assoc
:
This gem does exactly what is asked here: use EXISTS
to to do a condition.
It works with Rails 4.1 to the most recent.
members.where_assoc_exists(:responses)
It can also do much more!
Similar questions:
Upvotes: 123
Reputation: 71
If you are on Rails 5 and above you should use left_joins
. Otherwise a manual "LEFT OUTER JOINS" will also work. This is more performant than using includes
mentioned in https://stackoverflow.com/a/18234998/3788753. includes
will attempt to load the related objects into memory, whereas left_joins
will build a "LEFT OUTER JOINS" query.
def surveys_completed
members.left_joins(:responses).where.not(responses: { id: nil })
end
Even if there are no related records (like the query above where you are finding by nil) includes
still uses more memory. In my testing I found includes uses ~33x more memory on Rails 5.2.1. On Rails 4.2.x it was ~44x more memory compared to doing the joins manually.
See this gist for the test: https://gist.github.com/johnathanludwig/96fc33fc135ee558e0f09fb23a8cf3f1
Upvotes: 7
Reputation: 5807
You can also use a subquery:
members.where(id: Response.select(:member_id))
In comparison to something with includes
it will not load the associated models (which is a performance benefit if you do not need them).
Upvotes: 6
Reputation: 6545
You can use SQL EXISTS
keyword in elegant Rails-ish manner using Where Exists gem:
members.where_exists(:responses).count
Of course you can use raw SQL as well:
members.where("EXISTS" \
"(SELECT 1 FROM responses WHERE responses.member_id = members.id)").
count
Upvotes: 7