想在Rails 3中找到没有相关记录的记录

时间:2011-03-15 23:47:10

标签: ruby-on-rails arel meta-where

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

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

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

那么一个has_many:通过版本

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:通过

我不想把所有的person.friends记录并在Ruby中循环遍历 - 我希望有一个可以与meta_search gem一起使用的查询/范围

我不介意查询的性能成本

离实际SQL越远越好......

9 个答案:

答案 0 :(得分:391)

更好:

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

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

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

更新

在评论中有关于has_one的问题,所以只需更新。这里的诀窍是includes()期望关联的名称,但where期望表的名称。对于has_one,关联通常以单数形式表示,以便更改,但where()部分保持不变。因此,如果仅Person has_one :contact,那么您的陈述将是:

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

更新2

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

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

然后切换这个以返回没有人的朋友变得更简单,你只改变前面的班级:

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

更新3 - Rails 5

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

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

我把它包含在这里,所以人们会找到它,但他应该得到+ 1s。很棒的补充!

答案 1 :(得分:128)

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

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

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

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

答案 2 :(得分:95)

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

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

答案 3 :(得分:13)

没有朋友的人

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

或者至少有一个朋友

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

您可以通过在Friend

上设置范围来使用Arel执行此操作
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)

答案 4 :(得分:11)

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 / query

dmarkow的方法(:without_friends_v2)891ms /查询(慢10%)

但后来我发现我不需要调用DISTINCT()...我正在寻找PersonContacts的记录 - 所以他们只需要NOT IN 1}}联系人列表person_ids。所以我尝试了这个范围:

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

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

现在你可能在其他类似的查询中需要DISTINCT - 但对于我的情况,这似乎工作正常。

感谢您的帮助

答案 5 :(得分:5)

不幸的是,您可能正在寻找涉及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)

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

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

Friend.where.not(id: other_friend.friends.pluck(:id))

答案 8 :(得分:0)

以下是使用子查询的选项:

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