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

cra*_*com 171 ruby-on-rails arel meta-where

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

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end
Run Code Online (Sandbox Code Playgroud)

让所有在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
Run Code Online (Sandbox Code Playgroud)

我真的不想使用counter_cache - 而且我从我读过的内容中看起来并不适用于has_many:通过

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

我不介意查询的性能成本

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

sma*_*thy 411

更好:

Person.includes(:friends).where( :friends => { :person_id => nil } )
Run Code Online (Sandbox Code Playgroud)

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

Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Run Code Online (Sandbox Code Playgroud)

更新

has_one在评论中有一个问题,所以只是更新.这里的技巧是includes()期望关联的名称,但where期望表的名称.对于一个has_one关联通常会以单数表示,以便更改,但该where()部分保持不变.所以如果Person只有has_one :contact你的陈述是:

Person.includes(:contact).where( :contacts => { :person_id => nil } )
Run Code Online (Sandbox Code Playgroud)

更新2

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

Person.includes(:contacts).where( :contacts => { :id => nil } )
Run Code Online (Sandbox Code Playgroud)

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

Friend.includes(:contacts).where( :contacts => { :id => nil } )
Run Code Online (Sandbox Code Playgroud)

更新3 - Rails 5

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

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Run Code Online (Sandbox Code Playgroud)

我把它包括在这里,所以人们会找到它,但他应该得到+ 1s.太棒了!

  • @smathy Rails 6.1 中的一个很好的更新添加了一个“missing”方法来准确执行此操作(https://blog.saeloun.com/2020/01/21/rails-6-1-adds-query-method-missing -查找孤儿记录)! (10认同)
  • 是的,只是假设你的`has_one`关联有一个单数名称,你需要在`includes`调用中更改关联的名称.假设它是`Person`里面的`has_one:contact`,那么你的代码就是`Person.includes(:contact).where(:contacts => {:person_id => nil})` (5认同)
  • 您可以将其合并到一个更清洁的范围中. (4认同)
  • 更好的答案,不知道为什么另一个被评为接受. (3认同)
  • 如果您在Friend模型中使用自定义表名(`self.table_name ="custom_friends_table_name"`),则使用`Person.includes(:friends).where(:custom_friends_table_name => {:id => nil}) `. (3认同)
  • @smathy 这不适用于 has_one 关联。 (2认同)

Ans*_*son 154

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

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

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Run Code Online (Sandbox Code Playgroud)

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

  • 这种方法的最大优点是节省内存.当你执行`includes`时,所有这些AR对象都被加载到内存中,随着表越来越大,这可能是一件坏事.如果您不需要访问联系人记录,则`left_outer_joins`不会将联系人加载到内存中.SQL请求速度是相同的,但整体应用程序的好处要大得多. (3认同)
  • 这真的很棒!谢谢!现在,如果rails gods也许可以将它实现为一个简单的`Person.where(contacts:nil)`或`Person.with(contact:contact)`如果使用的地方侵入太过于'正确' - 但是给定的联系:是已经被解析并被识别为一个关联,似乎合乎逻辑的是,arel可以很容易地找出所需的... (2认同)

Uni*_*key 102

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

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Run Code Online (Sandbox Code Playgroud)

  • 试想一下,您的好友表中有1000万条记录。那情况下的性能呢? (4认同)

nov*_*ilo 13

没有朋友的人

Person.includes(:friends).where("friends.person_id IS NULL")
Run Code Online (Sandbox Code Playgroud)

或者至少有一个朋友

Person.includes(:friends).where("friends.person_id IS NOT NULL")
Run Code Online (Sandbox Code Playgroud)

您可以通过设置范围来使用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
Run Code Online (Sandbox Code Playgroud)

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

Person.includes(:friends).merge(Friend.to_somebody)
Run Code Online (Sandbox Code Playgroud)

无朋友:

Person.includes(:friends).merge(Friend.to_nobody)
Run Code Online (Sandbox Code Playgroud)

  • 我想你也可以这样做:Person.includes(:friends).where(朋友:{person:nil}) (2认同)

cra*_*com 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
Run Code Online (Sandbox Code Playgroud)

这是一个真正的应用程序 - 小表与约700'人'记录 - 平均5次运行

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

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

但后来我发现我不需要呼叫DISTINCT()...我正在寻找没有的Person记录Contacts- 所以他们只需要成为NOT IN联系人列表person_ids.所以我尝试了这个范围:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
Run Code Online (Sandbox Code Playgroud)

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

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

谢谢你的帮助


Dyl*_*kow 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
Run Code Online (Sandbox Code Playgroud)

然后为了得到它们,你可以做到Person.without_friends,你也可以用其他Arel方法链接它:Person.without_friends.order("name").limit(10)