Query that finds objects with ALL association ids (Rails 4) - sql

BACKGROUND: Posts have many Communities through CommunityPosts. I understand the following query returns posts associated with ANY ONE of these community_ids.
Post.joins(:communities).where(communities: { id: [1,2,3] })
OBJECTIVE: I'd like to query for posts associated with ALL THREE community_ids in the array. Posts having communities 1, 2, and 3 as associations
EDIT: Please assume that length of the array is unknown. Used this array for explanation purposes.

Try this,
ids=[...]
Post.joins(:communities).select(“count(communities.id) AS cnt”).where(id: ids).group(‘post.id’).having(cnt: ids.size)

ids = [1, 2, 3] # and etc
Post.joins(:communities).where("communities.id IN ?", ids)
Wish it helps .

Related

How to get id's from a list that are NOT in a table?

I am receiving a list of ID's. Most of these already exist in a table. I need to find which ID's are NOT in the table. This question has nothing to do with joins.
My API will receive a list of IDs, such as: [1, 2, 3, 4, 5]
Let's say there are three records in the table: [2, 3, 4]
The result I'm looking for is the array: [1, 5]
Our SQL brains jump quickly to something like the following, but clearly that's not what we need:
select * from widgets where id not in [list]
We don't need the records not in the list, we need the part of the list not in the records!
My fallback is to retrieve all records in the list and subtract from the list, something like this:
existing_ids = Widget.where(id: id_list).pluck(:id)
new_ids = id_list - existing_ids
That will work...but feels heavy-handed. Particularly if id_list has 100,000 records, and the table has 99,999 of those records.
I've searched around, and the only similar result is ID from list that is not in a table ... which did not find a viable solution.
Is there any way to do this in a single SQL query? (Bonus points for an ActiveRecord solution!)
To compare the lists to each other, either the input list needs to go into the database or the list of existing ids needs to come out of the database. The latter you already tried and didn't like, so here's an alternative
SELECT "id" FROM unnest('{1,2,3,4,5}'::integer[]) AS "id" WHERE "id" NOT IN (SELECT "id" FROM "widgets");
Not sure about performance.
Depending how many records are in your database, the simplest thing might just be to select all of the IDs and then drop the duplicates in Ruby.
from_api = [1,2,3,4,5]
existing = Widgets.pluck(:id) # => [2,3,4]
from_api.difference(existing) # => [1,5]
Obviously, if you have a substantial dataset, this will be less than optimal.
This should work.
from_api = [1,2,3,4,5]
existing = Widgets.order(:id).ids # => [2,3,4]
new_ids = []
from_api.each{ |n| new_ids << n unless existing.include? n }
new_ids # => [1,5]
or
from_api = [1,2,3,4,5]
existing = Widgets.order(:id).ids # => [2,3,4]
from_api.map{ |n| n == existing.first ? (nil if existing = existing.drop(1)) : n }.compac # => [1,5]
Balancing the complexity (to the current and future developers) of unset approach, I decided for my project that the simpler approach was warranted. While I didn't profile performance, I believe any gains would be minimal, if any.
Here is the solution I ended up with:
class Widget < ApplicationRecord
def self.absent(names)
uniq_names = names.uniq
uniq_names - where(name: uniq_names).pluck(:name)
end
end
And tests:
describe '.absent' do
subject { described_class.absent(names) }
let!(:widget1) { create(:widget, name: 'old-1') }
let!(:widget2) { create(:widget, name: 'old-2') }
let(:names) { %w[new-2 old-2 new-1 old-1 new-1 old-1] }
it { is_expected.to eq %w[new-2 new-1] }
end

Rails count values returned from pluck

I am building a rails application, and I need to create some charts.
I am running this query to retrieve the answers from the user:
quiz = Quiz.select("answer1").where(completed: true).pluck(:answer1)
And the query returns for me this: [1, 2, 1, 1, 1]
I want to count the values and group them like this: { 1 => 4, 2 => 1 }
I have tried to use group by and count but it is not working, I could do this manually but I wanted to use just SQL to achieve this.
I remember to use group by and count using sql, but I am not sure how to do this using rails.
You can group('answer1') as described here
Quiz.where(completed: true).group('answer1').count
Hope it helped!
Try this one
Quiz.where(completed: true).group(:answer1).count(:answer1)

Merge the results of two active record queries

I want to merge the results from the following queries:
Dispenser.includes(:reviews).where.not(reviews: { id: nil })
and
Dispenser.includes(:dispenser_reviews).where.not(dispenser_reviews: { id: nil })
I have tried simply placing a || in the middle of these two queries, but that does not give the expected result. I want to find all Dispensers with a review or a dispenser_review..
So let's say I have the following dispenser ids from each query:
[1, 2, 3] and [2, 3, 4] ..
The output should be the dispensers represented by the ids [1, 2, 3, 4]
You can accomplish that using https://github.com/activerecord-hackery/squeel rather than active record. It provides the more advanced functionality that Arel does not have out of the box.
That being said, your logic is gonna be pretty nasty. If you wanted to get the result set and you didn't mind two queries instead of one, I'd just join the two results with the + operator.
r1 = Dispenser.includes(:reviews)# ...
r2 = Dispenser.includes(:dispenser_reviews)# ...
result = r1 + r2
As for a squeel example, it'd be something like:
Dispenser.includes{reviews}.
includes{dispenser_reviews}.
where{(reviews.id.not_eq nil) | {dispenser_reviews.id.not_eq nil)}.
references(:all)
Joins will do an INNER JOIN and only return dispenser objects that have reviews or dispenser_reviews. Pipe '|' will get rid of dups.
Dispenser.joins(:reviews) | Dispenser.joins(:dispenser_reviews)
or to get ids
Dispenser.joins(:reviews).pluck(:id) | Dispenser.joins(:dispenser_reviews).pluck(:id)
You can also use arel and combine the two queries into one like this:
Dispenser.includes(:reviews, :dispenser_reviews).where((Review.arel_table[:id].not_eq(nil)).or DispenserReview.arel_table[:id].not_eq(nil)).references(:reviews, :dispenser_reviews)

AND multiple JOIN statements in Rails

I have a table for users and roles. I'm using a has many through relationship. I am trying to create a query that will find users that have all of the roles in an array.
ex.
role_ids = [2, 4, 6]
User.filter(role_ids) would return all users that have roles with ids 2, 4, 6.
This is what I have so far.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} )
end
The problem with this statement is it returns all users who have at least one of the roles in role_ids.
How do I make this statement give me an intersection, not a union?
I think you are asking for only unique instances of users who meet the role filter criteria. If so, then this should work.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} ).uniq
end

Can you select unique by count in Rails Active Record?

If you can't do it, how would SQL do it?
Basically I want to select all my question objects where there is at least two with the same attribute. That attribute is called, lets say, word_id.
So how would I only select all the objects that that share only once a common attribute with another object?
If I have three objects :
# Question(id: 1, word_id: 1)
# Question(id: 2, word_id: 2)
# Question(id: 3, word_id: 2)
# Question(id: 4, word_id: 1)
# Question(id: 5, word_id: 1)
# Question(id: 6, word_id: 1)
I would want to return just id's 2 and 3 since they both share a common attribute twice.
Is that possible? I crudely do this by making two calls to the DB where first I call all the objects in question, add them to an array, and subtract from that array objects that match my requirements. I was just curious if there was a more elegant way to do it all at once.
Just SQL:
SELECT * FROM questions WHERE world_id IN (
SELECT world_id FROM questions GROUP BY world_id HAVING count(*) = 2
)
Rails:
Question.where("world_id IN (?)", Question.find(:all, select: "world_id",
group: "world_id HAVING count(*) = 2"))
I guess that's still two queries though...