AND multiple JOIN statements in Rails - sql

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

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 4 ActiveRecord: Order recrods by attribute and association if it exists

I have three models that I am having trouble ordering:
User(:id, :name, :email)
Capsule(:id, :name)
Outfit(:id, :name, :capsule_id, :likes_count)
Like(:id, :outfit_id, :user_id)
I want to get all the Outfits that belong to a Capsule and order them by the likes_count.
This is fairly trivial and I can get them like this:
Outfit.where(capsule_id: capsule.id).includes(:likes).order(likes_count: :desc)
However, I then want to also order the outfits so that if a given user has liked it, it appears higher in the list.
Example if I have the following outfit records:
Outfit(id: 1, capsule_id: 2, likes_count: 1)
Outfit(id: 2, capsule_id: 2, likes_count: 2)
Outfit(id: 3, capsule_id: 2, likes_count: 2)
And the given user has only liked outfit with id 3, the returned order should be IDs: 3, 2, 1
I'm sure this is fairly easy, but I can't seem to get it. Any help would be greatly appreciated :)
Postgres SQL with a subquery
SELECT outfits.*
FROM outfits
LEFT OUTER JOIN (SELECT likes.outfit_id, 1 AS weight
FROM likes
WHERE likes.user_id = #user_id) AS user_likes
ON user_likes.outfit_id = outfits.id
WHERE outfits.capsule_id = #capsule_id
ORDER BY user_likes.weight ASC, outfits.likes_count DESC;
Postgres gives NULL values bigger weight when ordering. I am not sure how this would look in Arel query. You can try converting it using this cheatsheets.

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)

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

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 .

rails 3 creating an index from two models using where clause

quick rails section, say I have two models Users and Roles and I want to create an index/list of Users based on a certain Role, how do I go about building that in my controller
is it something like
#first create the association
#user = role.build
#then build the index based on a Role of role_id = 2
#userrole = #user.where(#user.role_id == 2)
I know this is pseudo code, but is this correct? And what is the proper rails code?
roles = Role.where(id: [1, 2])
users = User.where(role_ids: roles.collect(&:ids))
This is just an example because obviously if we already know the ids we wouldn't need the first query but if our roles had an editable column (as an example) we could do:
roles = Role.where(editable: true)
users = User.where(role_ids: roles.collect(&:ids))
And if we wanted to order these we could simply add:
users = User.where(role_ids: roles.collect(&:ids)).order("created_at DESC")