考虑一个简单的关联……

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

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

然后是has_many:through version

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越远越好……


当前回答

下面是一个使用子查询的选项:

# 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
)

其他回答

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

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')

NOT EXISTS相关子查询应该很快,特别是当行数和子记录与父记录之比增加时。

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")

下面是一个使用子查询的选项:

# 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
)

不幸的是,你可能正在寻找一个涉及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.order("name").limit(10)

更新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

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

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

然后切换到返回没有人的朋友变得更简单,你只改变前面的类:

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

更新

在评论中有一个关于has_one的问题,所以只是更新。这里的技巧是includes()期望关联的名称,而where期望表的名称。对于has_one,关联通常用单数表示,因此会发生变化,但where()部分保持不变。所以如果Person只有_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 })