Guillaume
Guillaume

Reputation: 1537

ActiveRecord AND query

I have 3 models : User, Conversation and ConversationUser

class User < ApplicationRecord
  has_many :conversation_users, dependent: :destroy
  has_many :conversations, through: :conversation_users
end
class Conversation < ApplicationRecord
  has_many :conversation_users, dependent: :destroy
  has_many :users, through: :conversation_users
end
class ConversationUser < ApplicationRecord
  belongs_to :conversation
  belongs_to :user
end

My purpose

I would like to find a Conversation when 2 users have the same conversation (a conversation must only have 2 conversation_users with the id of these 2 users)

For the test

I only created ONE conversation with TWO conversation_users

conversation = Conversation.create
conversation.conversation_users.create(user: User.first)
conversation.conversation_users.create(user: User.second)

My Problem

I want to find the conversation with JUST the first User AND second User. If I do the following query, Active Record will still show me a conversation because it's doing a OR clause but I need a AND. The correct result should show me an empty array because there is not a conversation with User.first AND User.third

Conversation.joins(:conversation_users).where(conversation_users: {user_id: [User.first.id, User.third.id]})

  User Load (3.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1  [["LIMIT", 1]]
  User Load (2.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2  [["LIMIT", 1], ["OFFSET", 2]]
  Conversation Load (3.9ms)  SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" IN ($1, $2) LIMIT $3  [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "0ef2f797-3518-4096-951b-1837ca28e4e4"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Conversation id: "35556705-a162-4ee4-9776-963ae87bcfa8", created_at: "2020-04-20 09:34:05", updated_at: "2020-04-20 09:34:05">]>

Expectations

I tried this query for the first expectation but it is returning an empty array

Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})

  User Load (3.8ms)  SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1  [["LIMIT", 1]]
  User Load (4.0ms)  SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2  [["LIMIT", 1], ["OFFSET", 1]]
  Conversation Load (3.3ms)  SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" = $1 AND "conversation_users"."user_id" = $2 LIMIT $3  [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "44a0c35a-b5c1-4bb6-a8a3-30ae3d4b3345"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>

Upvotes: 0

Views: 39

Answers (2)

max
max

Reputation: 102026

class Conversation < ApplicationRecord
  has_many :user_conversations
  has_many :users, through: :user_conversations

  def self.between(*users)
    users.map do |u|
      where("EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = ?)", u)
    end.reduce(&:merge)
      .joins(:user_conversations)
      .group(:id)
      .having('count(*) = ?', users.length)
  end
end

Not the cleanest solution ever but what is does is check for the existance of a join record for each user and then .having('count(*) = ?', users.length) ensures that there are not more join records then users.

Example usage:

Conversation.between(User.first, User.second)
Conversation.between(1,2,3)
Conversation.between(*User.all) # splat it like a pro

This results in the following SQL query:

SELECT "conversations".* FROM "conversations" 
INNER JOIN "user_conversations" ON "user_conversations"."conversation_id" = "conversations"."id" 
WHERE 
  (EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 1))
  AND 
  (EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 2)) 
GROUP BY "conversations"."id"
HAVING (count(*) = 2) 
LIMIT $1

Upvotes: 1

Sajjad Umar
Sajjad Umar

Reputation: 141

You can do a query with AND condition as follows

Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})

Upvotes: 0

Related Questions