Ruby on rails 要在Rails中查找没有关联记录的记录吗

Ruby on rails 要在Rails中查找没有关联记录的记录吗,ruby-on-rails,arel,meta-where,Ruby On Rails,Arel,Meta Where,考虑一个简单的关联 class Person has_many :friends end class Friend belongs_to :person end 让所有在ARel和/或meta_没有朋友的人去哪里最干净的方法是什么 那么一个有很多的版本呢 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true end class Fri

考虑一个简单的关联

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end
让所有在ARel和/或meta_没有朋友的人去哪里最干净的方法是什么

那么一个有很多的版本呢

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_搜索gem

我不介意查询的性能成本


距离实际SQL越远越好…

不幸的是,您可能正在寻找一种涉及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\u friends
,你也可以将其与其他Arel方法链接:
Person.without\u friends.order(“name”).limit(10)

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

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

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的方法(
:没有朋友的情况下v1
)813ms/query

dmarkow的方法(
:没有朋友的情况下)891ms/查询(~10%慢)

但后来我突然想到,我不需要调用
DISTINCT()…
我正在寻找没有
联系人的
个人
记录,所以他们只需要
不在联系人
个人ID的列表中。所以我尝试了这个范围:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
这得到了相同的结果,但平均每次通话425毫秒-几乎一半的时间

现在,您可能需要在其他类似查询中使用
DISTINCT
,但对于我的情况,这似乎很好

感谢您的帮助

更新4-Rails 6.1 感谢您指出,在即将发布的6.1中,您可以做到以下几点:

Person.where.缺失(:联系人)
多亏了他也联系到了

更新3-Rails 5 感谢@Anson提供了出色的Rails 5解决方案(下面的答案请给他一些+1),您可以使用
左外连接来避免加载关联:

Person.left_outer_joins(:contacts).where(contacts: { id: nil })
我把它包括在这里,这样人们会找到它,但他应该为此得到+1。添加得太好了

更新2 有人问起相反的问题,没有人的朋友。正如我在下面所评论的,这实际上使我意识到最后一个字段(上面的
:person\u id
)实际上不必与您返回的模型相关,它只需要是联接表中的一个字段。它们都将是
nil
,因此可以是它们中的任何一个。这就为上述问题提供了一个更简单的解决方案:

Person.includes(:contacts)。其中(contacts:{id:nil})
然后切换此选项以返回无人好友变得更简单,您只需更改前面的类:

Friend.includes(:contacts).where(contacts:{id:nil})
更新 在评论中有一个关于
的问题,请更新。这里的技巧是
includes()
需要关联的名称,而
where
需要表的名称。对于
has_one
而言,关联通常用单数表示,因此会发生变化,但
where()
部分保持原样。因此,如果一个
只有
有一个:联系人
,那么你的陈述是:

Person.includes(:contact).where(contacts:{Person\u id:nil})
起初的 更好:

Person.includes(:friends).where(friends:{Person\u id:nil})
对于hmt,这基本上是一样的,你依赖于一个没有朋友的人也没有联系人的事实:

Person.includes(:contacts)。其中(contacts:{Person\u id:nil})

一个不存在的相关子查询应该很快,尤其是当行数和子记录与父记录的比率增加时

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

没有朋友的人

Person.includes(:friends).where("friends.person_id IS NULL")
或者至少有一个朋友

Person.includes(:friends).where("friends.person_id IS NOT NULL")
通过在
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)

斯马蒂有一个很好的答案

对于Rails 5,您可以使用
左外连接来避免加载关联

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

看看这本书。它是在pull请求中引入的。

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

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

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

#场景#一人好友
people=Person.where.not(id:Friend.select(:Person\u id))
#场景#2人联系朋友
people=Person.where.not(id:Contact.select(:Person\u id))
上述表达式应生成以下SQL:

——场景#一人好友
选择人*
来自人们
我不在的地方(
选择friends.person\u id
来自朋友
)
--场景#2人联系朋友
选择人*
来自人们
我不在的地方(
选择contacts.person\u id
来自联系人
)

您可以将其合并到一个更干净的范围中。更好的答案是,不确定为什么另一个被评为可接受。是的,假设您的
有一个
关联的单数名称,您需要在
includes
调用中更改关联的名称。因此,假设它是
Person
中有一个:contact
那么你的代码就是
Person.includes(:contact)。其中(:contacts=>{:Person\u id=>nil})
如果你在你的朋友模型中使用一个自定义表名(
self.table\u name=“custom\u friends\u table\u name”
),那么就使用
Person.includes(:friends)。其中(:custom_friends_table_name=>{:id=>nil})
@smat