Rails:如何 Select 没有特定相关(关联)对象的记录(SQL EXISTS 简要操作方法)

Rails: How to Select Records Which Don't Have a Specific Related (associated) Object (SQL EXISTS brief how-to)

假设我们有用户:

class User < ActiveRecord::Base
  has_many :connections
  has_many :groups, through: :connections
end

和群组:

class Group < ActiveRecord::Base
  has_many :connections
  has_many :users, through: :connections
end

基本上,标准的多对多连接:

class Connection
  belongs_to :user
  belongs_to :group
end

我打算做的是:

此外,我不想:

此类问题在初级到中级 Rails 开发人员中很常见。您知道 ActiveRecord 界面和基本 SQL 操作,但您无意中遇到了问题中概述的这类任务。 (此类问题的几个示例:, )。

答案很简单:使用SQLEXISTS condition。给定 URL:

的快速参考

Syntax

The syntax for the SQL EXISTS condition is:

WHERE EXISTS ( subquery );

Parameters or Arguments

subquery

The subquery is a SELECT statement. If the subquery returns at least one record in its result set, the EXISTS clause will evaluate to true and the EXISTS condition will be met. If the subquery does not return any records, the EXISTS clause will evaluate to false and the EXISTS condition will not be met.

还提到 EXISTS 可能比 JOIN 慢,但这通常不是真的。来自 Exists v. Join 关于 SO 的问题:

EXISTS is only used to test if a subquery returns results, and short circuits as soon as it does. JOIN is used to extend a result set by combining it with additional fields from another table to which there is a relation. [...] If you have proper indexes, most of the time the EXISTS will perform identically to the JOIN. The exception is on very complicated subqueries, where it is normally quicker to use EXISTS.

因此,数据库不需要查看所有连接(一旦找到正确的连接,它就会停止使用 'exists' 的 'joining' 记录),也不需要return 来自 table 的所有字段都已加入(只需检查相应的行是否存在)。

回答具体问题:

Select only such users, who don't belong to given set of Groups (groups with ids [4,5,6])

not_four_to_six = User.where("NOT EXISTS (
   SELECT 1 FROM connections
   WHERE connections.user_id = users.id
   AND connections.group_id IN (?)
  )", [4,5,6])

Select only such users, who belong to one set of Groups ([1,2,3]) and don't belong to another ([4,5,6])

one_two_three = not_four_to_six.where("EXISTS (
   SELECT 1 FROM connections
   WHERE connections.user_id = users.id
   AND connections.group_id IN (?)
  )", [1,2,3])

Select only such users, who doesn't belong to a Group

User.where("NOT EXISTS (
   SELECT 1 FROM connections
   WHERE connections.user_id = users.id
  )")