Rails中如何编写not_voted_by作用域获取未被指定用户投票的电影?
not_voted_by作用域在多投票场景下的失效问题 这个问题我之前也碰到过,核心原因是你当前的实现没考虑到电影存在多条投票记录的情况——当一部电影被多个用户投票时,where.not(votes: { user: user })会匹配到其他用户的投票行,导致这部电影仍然被错误地包含在结果里。咱们来拆解下问题,再给出靠谱的解决方案:
问题根源
举个例子:电影M被用户A和B同时投票。当你调用Movie.not_voted_by(A)时,你的查询会关联出M的两条投票记录(A和B的),其中B的投票记录满足votes.user != A,所以这条记录会被保留,最终M就出现在了结果里,这显然不符合你要“排除该用户投票过的所有电影”的需求。
我们真正要的是:所有不存在当前用户投票记录的电影,包括从来没被任何人投过的电影。
解决方案
方案1:用子查询直接排除已投票的电影(最推荐)
这个写法简洁直观,性能也不错:先找出该用户所有投票过的电影ID,然后直接排除这些ID即可。
class Movie scope :not_voted_by, ->(user) { where.not(id: user.votes.select(:movie_id)) } end
为什么这个方案好?
- 逻辑一目了然:就是排除用户已经投过票的电影
- 性能高效:数据库会利用
votes表上的user_id和movie_id索引快速获取已投票的电影ID集合,再做排除 - 自动处理“从未被投票的电影”:这类电影的ID不在子查询结果里,自然会被保留
方案2:用LEFT JOIN + 空值判断
如果你更倾向于用关联查询的方式,可以通过左连接投票表,筛选出没有匹配到当前用户投票的电影:
class Movie scope :not_voted_by, ->(user) { left_joins(:votes) .where("votes.user_id != ? OR votes.id IS NULL", user.id) .distinct } end
这里的distinct很关键,因为如果一部电影有多个其他用户的投票,左连接会生成多条重复的电影记录,distinct可以帮我们去重,确保每部电影只出现一次。
验证效果
拿你提到的场景测试:电影M被A和B投票,调用Movie.not_voted_by(A)时,方案1会先获取A投票过的电影ID(包括M),然后排除这些ID,所以M不会出现在结果里;方案2通过左连接后,M的投票记录里只有B的那条满足votes.user_id != A,但因为distinct会去重,最终M不会被包含进来——两种方案都能得到符合预期的结果。
内容的提问来源于stack exchange,提问作者Jun Zhou

