209

考虑一个简单的关联......

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

让所有在 ARel 和/或 meta_where 中没有朋友的人最干净的方法是什么?

那么 has_many :through 版本呢

class Person
   has_many :contacts
   has_many :friends, :through => :contacts, :uniq => true
end

class Friend
   has_many :contacts
   has_many :people, :through => :contacts, :uniq => true
end

class Contact
   belongs_to :friend
   belongs_to :person
end

我真的不想使用 counter_cache - 从我读过的内容来看,它不适用于 has_many :through

我不想提取所有 person.friends 记录并在 Ruby 中循环它们 - 我想要一个可以与 meta_search gem 一起使用的查询/范围

我不介意查询的性能成本

离实际 SQL 越远越好……

4

9 回答 9

522

更新 4 - Rails 6.1

感谢Tim Park指出在即将到来的 6.1 中你可以这样做:

Person.where.missing(:contacts)

感谢他也链接到的帖子。

更新 3 - Rails 5

感谢@Anson 提供了出色的 Rails 5 解决方案(在下面给他一些 +1 的答案),您可以使用它left_outer_joins来避免加载关联:

Person.left_outer_joins(:contacts).where(contacts: { id: nil })

我已将其包含在此处,以便人们找到它,但他应该为此获得 +1。伟大的补充!

更新 2

有人问逆,朋友无人。正如我在下面评论的那样,这实际上让我意识到最后一个字段(上图::person_id)实际上不必与您返回的模型相关,它只需是连接表中的一个字段。他们都将是,nil所以它可以是他们中的任何一个。这导致了对上述问题的更简单的解决方案:

Person.includes(:contacts).where(contacts: { id: nil })

然后将其切换为返回没有人的朋友变得更加简单,您只需更改前面的类:

Friend.includes(:contacts).where(contacts: { id: nil })

更新

有问题has_one在评论里,所以只是更新。这里的技巧是includes()期望关联的名称,但where期望表的名称。对于 a 而言has_one,关联通常以单数形式表示,因此会发生变化,但where()部分保持不变。因此,如果Person只有一个has_one :contact,那么您的陈述将是:

Person.includes(:contact).where(contacts: { person_id: nil })

原来的

更好的:

Person.includes(:friends).where(friends: { person_id: nil })

对于 hmt 基本上是一样的,你依赖于一个没有朋友的人也没有联系人的事实:

Person.includes(:contacts).where(contacts: { person_id: nil })
于 2011-04-06T17:05:10.873 回答
186

smathy 有一个很好的 Rails 3 答案。

对于 Rails 5,您可以使用left_outer_joins来避免加载关联。

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

查看api 文档。它是在拉取请求#12071中引入的。

于 2016-11-09T16:19:01.390 回答
124

这仍然非常接近 SQL,但它应该让每个没有朋友的人在第一种情况下:

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
于 2011-03-16T00:33:29.780 回答
14

没有朋友的人

Person.includes(:friends).where("friends.person_id IS NULL")

或者至少有一个朋友

Person.includes(:friends).where("friends.person_id IS NOT NULL")

您可以通过在 Arel 上设置范围来做到这一点Friend

class Friend
  belongs_to :person

  scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
  scope :to_nobody,   ->{ where arel_table[:person_id].eq(nil) }
end

然后,至少有一个朋友的人:

Person.includes(:friends).merge(Friend.to_somebody)

无友者:

Person.includes(:friends).merge(Friend.to_nobody)
于 2013-09-29T16:05:09.353 回答
12

dmarkow 和 Unixmonkey 的答案都让我得到了我需要的东西——谢谢!

我在我的真实应用程序中尝试了这两种方法并获得了它们的时间 - 这是两个范围:

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
  scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end

用一个真正的应用程序运行这个 - 大约 700 条“人员”记录的小表 - 平均运行 5 次

Unixmonkey 的方法 ( :without_friends_v1) 813ms / 查询

dmarkow 的方法 ( :without_friends_v2) 891 毫秒 / 查询 (~ 10% 慢)

但后来我突然想到,我不需要打电话给DISTINCT()...我正在寻找PersonNO 的记录Contacts- 所以他们只需要成为NOT IN联系人列表person_ids。所以我尝试了这个范围:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }

这得到了相同的结果,但平均为 425 毫秒/调用 - 几乎一半的时间......

现在您可能需要DISTINCT在其他类似的查询中 - 但就我而言,这似乎工作正常。

谢谢你的帮助

于 2011-03-16T15:17:21.440 回答
6

不幸的是,您可能正在寻找一个涉及 SQL 的解决方案,但您可以将其设置在一个范围内,然后只使用该范围:

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end

然后要获取它们,您可以这样做Person.without_friends,您也可以将其与其他 Arel 方法链接:Person.without_friends.order("name").limit(10)

于 2011-03-16T00:29:54.490 回答
1

NOT EXISTS 相关子查询应该很快,尤其是当行数和子记录与父记录的比率增加时。

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
于 2013-09-16T08:40:28.590 回答
1

这是使用子查询的选项:

# Scenario #1 - person <-> friend
people = Person.where.not(id: Friend.select(:person_id))

# Scenario #2 - person <-> contact <-> friend
people = Person.where.not(id: Contact.select(:person_id))

上述表达式应生成以下 SQL:

-- Scenario #1 - person <-> friend
SELECT people.*
FROM people 
WHERE people.id NOT IN (
  SELECT friends.person_id
  FROM friends
)

-- Scenario #2 - person <-> contact <-> friend
SELECT people.*
FROM people 
WHERE people.id NOT IN (
  SELECT contacts.person_id
  FROM contacts
)
于 2020-10-16T14:07:02.330 回答
1

此外,例如,要被一位朋友过滤掉:

Friend.where.not(id: other_friend.friends.pluck(:id))
于 2017-06-01T23:53:13.993 回答