Jun Zhou
Jun Zhou

Reputation: 3080

How can I write a scope to return all movies that are `not_voted_by` given user?

In rails I have 3 models: Movie, Vote and User

class Movie
  has_many :votes
end

class User
  has_many :votes
end

class Vote
  belongs_to :movie
  belongs_to :user
end

I have a scope voted_by for model Movie, which returns all movies voted by given user.

class Movie
  scope :voted_by, ->(user) {
    includes(:votes).where(votes: { user: user })
  }
end

Now I want another scope not_voted_by which does exact the opposite: it should returns all movies that are NOT voted by given user.

How can I achieve that?

What I come up with so far is:

scope :not_voted_by, ->(user) {
  includes(:votes).where.not(votes: { user: user }).or(
    includes(:votes).where(votes: { user: nil })
  )
}

It works when a movie is voted by one or zero user. But it doesn't work when the movie is voted by multiple users.

For example if movie M is voted by both user A and B. Movie.not_voted_by(A) still includes M while it shouldn't.

Thanks in advance!

Upvotes: 1

Views: 32

Answers (1)

Artem Dorodovskyi
Artem Dorodovskyi

Reputation: 679

Includes in SQL means left join. So you add all records in the left where user not a user. SQL

SELECT *
FROM movies
LEFT JOIN votes ON (votes.movie_id = movies.id)
WHERE votes.user_id <> 1

If you have e.g. in table.

id | movie_id | user_id
-----------------------
1  | 1        | 1
2  | 1        | 2
3  | 2        | 2

Then you query exclude only first record if user.id = 1.

So, I think you need to exclude voted movies by negate previous scope

scope :not_voted_by, ->(user) { where.not(id: voted_by(user) }

Upvotes: 3

Related Questions