想要在Rails中查找没有关联记录的记录


177

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

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距离越远越好...

Answers:


110

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

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

6
试想一下,您的好友表中有1000万条记录。那情况下的性能呢?
goodniceweb

@goodniceweb根据您的重复频率,您可以删除DISTINCT。否则,我认为您想在这种情况下规范化数据和索引。我可以通过创建friend_idshstore或序列化列来做到这一点。然后您可以说Person.where(friend_ids: nil)
Unixmonkey '16

如果您要使用sql,则最好使用sql not exists (select person_id from friends where person_id = person.id)(或者也许是people.idor persons.id,具体取决于您的表是什么。)不确定在特定情况下最快的是什么,但是在过去,当我使用这种方法时,对我来说效果很好没有尝试使用ActiveRecord。
nroose

441

更好:

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

我将其包含在此处,以便人们找到它,但是他值得为此加+1。很棒的补充!

更新4-Rails 6.1

感谢Tim Park指出,在即将到来的6.1中,您可以执行以下操作:

Person.where.missing(:contacts)

多亏的帖子,他也链接到了。


4
您可以将其合并到一个范围内,这样会更加干净。
Eytan 2012年

3
更好的答案,不确定为什么其他人被评为接受。
Tamik Soziev

5
是的,只是假设您有一个has_one关联的唯一名称,您需要在includes呼叫中更改关联的名称。因此,假设它在has_one :contact内部,Person则您的代码将是Person.includes(:contact).where( :contacts => { :person_id => nil } )
smathy 2013年

3
如果您在Friend模型(self.table_name = "custom_friends_table_name")中使用自定义表格名称,请使用Person.includes(:friends).where(:custom_friends_table_name => {:id => nil})
Zek

5
@smathy Rails 6.1中的一个不错的更新添加了missing一种完全可以做到这一点的方法!
蒂姆·帕克

171

smathy对Rails 3的回答很好。

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

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

查看api文档。在请求请求#12071中引入了它。


这有什么缺点吗?我检查了一下,然后加载了0.1 ms,然后加载速度更快
-Qwertie

如果您稍后实际访问它,则不加载该关联是一个缺点,但如果您不访问它,则是一个好处。对于我的网站,0.1ms的命中率几乎可以忽略不计,因此.includes,加载时间的额外开销对于优化我不会太担心。您的用例可能有所不同。
安森

1
而且,如果您还没有Rails 5,您可以这样做:Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')它也可以作为示波器正常工作。我一直在Rails项目中这样做。
Frank

3
这种方法的最大优点是节省了内存。当您执行时includes,所有这些AR对象都将加载到内存中,随着表越来越大,这可能是一件坏事。如果您不需要访问联系人记录,left_outer_joins则不会将联系人加载到内存中。SQL请求速度是相同的,但是总体应用程序收益要大得多。
chrismanderson

2
这真的很好!谢谢!现在,如果铁道之神可能将其实现为简单的方法,Person.where(contacts: nil)或者Person.with(contact: contact)如果使用过于侵犯“特性”的位置-但鉴于该接触:已经被解析并确定为关联,那么逻辑上可以轻松地确定arel是合理的...
贾斯汀·麦克斯韦

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)

2
我认为您也可以这样做:Person.includes(:friends).where(friends:{person:nil})
ReggieB 2014年

1
注意:合并策略有时可能会发出警告,例如DEPRECATION WARNING: It looks like you are eager loading table(s) Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
genkilabs 2015年

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)891ms /查询(慢10%)

但是后来我想到,我不需要打DISTINCT()...我要查找Person带有NO的记录的电话Contacts-因此它们只需要NOT IN作为联系人列表person_ids。所以我尝试了这个范围:

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

结果相同,但平均每次通话425毫秒-几乎是一半的时间...

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

谢谢你的帮助


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)


1

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

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

1

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

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

3
这将导致2个查询,而不是子查询。
grepsedawk
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.