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

Posted

技术标签:

【中文标题】想在 Rails 中查找没有关联记录的记录【英文标题】:Want to find records with no associated records in Rails 【发布时间】:2011-07-16 05:15:48 【问题描述】:

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

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

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

我不介意查询的性能成本

离实际 SQL 越远越好...

【问题讨论】:

【参考方案1】:

更新 4 - Rails 6.1

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

Person.where.missing(:contacts)

感谢the post,他也链接到了。

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

更新

在 cmets 中有一个关于 has_one 的问题,所以更新一下。这里的技巧是 includes() 需要关联的名称,但 where 需要表的名称。对于has_one,关联通常以单数形式表示,因此会发生变化,但where() 部分保持不变。因此,如果 Personhas_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 )

【讨论】:

您可以将其合并到一个更干净的范围中。 更好的答案,不知道为什么另一个被评为接受。 是的,只是假设您的has_one 关联有一个单数名称,您需要在includes 调用中更改关联名称。所以假设它是has_one :contact 里面Person 那么你的代码就是Person.includes(:contact).where( :contacts => :person_id => nil ) 如果您在 Friend 模型中使用自定义表名 (self.table_name = "custom_friends_table_name"),请使用 Person.includes(:friends).where(:custom_friends_table_name => :id => nil) @smathy Rails 6.1 中一个不错的更新添加了一个 missing 方法来完全做到 this!【参考方案2】:

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

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

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

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

【讨论】:

这有什么缺点吗?我检查了一下,它的加载速度比 .includes 快 0.1 毫秒 如果你还没有 Rails 5,你可以这样做:Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL') 它也可以作为一个作用域。我在我的 Rails 项目中一直这样做。 这种方法的最大优点是节省内存。当您执行includes 时,所有这些 AR 对象都会加载到内存中,随着表变得越来越大,这可能是一件坏事。如果您不需要访问联系人记录,left_outer_joins 不会将联系人加载到内存中。 SQL 请求速度相同,但整体应用收益要大得多。 这真是太好了!谢谢!现在,如果 rails 之神可以将其实现为简单的 Person.where(contacts: nil)Person.with(contact: contact),如果使用 where 侵犯了“适当性”太远 - 但鉴于 contact: 已经被解析并识别为关联,这似乎是合乎逻辑的可以很容易地计算出需要什么...... @max 是的,我的意思是说 .left_joins.left_outer_joins 的别名 - 这两者都创建相同的 LEFT OUTER JOINS sql【参考方案3】:

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

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

【讨论】:

想象一下你在朋友表中有 10000000 条记录。在这种情况下性能如何? @goodniceweb 根据您的重复频率,您可能会放弃DISTINCT。否则,我认为您希望在这种情况下规范化数据和索引。我可以通过创建friend_ids hstore 或序列化列来做到这一点。然后你可以说Person.where(friend_ids: nil) 如果你要使用sql,最好使用not exists (select person_id from friends where person_id = person.id)(或者people.idpersons.id,取决于你的表是什么。)不确定最快的是什么在特定情况下,但在过去,当我不尝试使用 ActiveRecord 时,这对我来说效果很好。【参考方案4】:

没有朋友的人

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)

【讨论】:

我觉得你也可以这样做:Person.includes(:friends).where(friends: person: nil) 注意:合并策略有时会产生类似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的警告【参考方案5】:

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()... 我正在寻找Person 没有Contacts 的记录 - 所以他们只需要NOT IN 联系人列表@ 987654328@。所以我尝试了这个范围:

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

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

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

感谢您的帮助

【讨论】:

【参考方案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)

【讨论】:

【参考方案7】:

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

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

【讨论】:

【参考方案8】:

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

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

【讨论】:

这将产生 2 个查询而不是一个子查询。【参考方案9】:

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

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

【讨论】:

以上是关于想在 Rails 中查找没有关联记录的记录的主要内容,如果未能解决你的问题,请参考以下文章

硬删除在rails中启用软删除的关联记录

在 rails 中显示关联的记录

如何在 Rails 2.3.8 中使用带有关联的 Active 脚手架列出活动记录?

在Rails 4.1中,如何通过枚举符号查找记录?

如何在整个 Rails 活动记录关联链中取消特定模型的默认范围?

为啥 Rails 在通过关联创建新记录时不更新其关联缓存?