Rails作用域用于检查关联是否不存在。

33
我计划编写一个作用域(scope),它返回所有没有特定关联的记录。 foo.rb
class Foo < ActiveRecord::Base    
  has_many :bars
end

bar.rb

class Bar < ActiveRecord::Base    
  belongs_to :foo
end

我想要一个作用域(scope),可以找到所有没有任何barsFoo。使用joins可以轻松找到有关联的,但我还没有找到相反的方法。

6个回答

47

Rails 4 让这变得太容易了 :)

Foo.where.not(id: Bar.select(:foo_id).uniq)

这将输出与jdoe答案相同的查询

SELECT "foos".* 
FROM "foos" 
WHERE "foos"."id" NOT IN (
  SELECT DISTINCT "bars"."foo_id"
  FROM "bars" 
)

并且作为范围:

scope :lonely, -> { where.not(id: Bar.select(:item_id).uniq) }

1
我必须使用.pluck(:foo_id)而不是.select(:foo_id)。你的方法对我无效,因为在模型上调用.select会返回一个ActiveRecord_Relation而不是一个数组中的id,这就是你想要的Model.where.not(id: [1, 2, ...]语句所需要的吗? - Bryan Dimas
2
是的,虽然.select返回一个ActiveRecord关系,但.pluck返回一个Ruby数组。但是当作为.select的子查询添加时,由于Rails自动将其与其他查询组合,因此您只需调用一次DB。使用.pluck会调用两次DB。此外,请记住,作用域通常链接在一起,因此不建议使用.pluck - davegson
也许它不能为我工作的原因是因为我现在正在使用SQLite(我正在开发一个小程序)。 - Bryan Dimas
是的,如果这是一个小项目,在调用数据库两次方面可能不会有太大问题 :) - davegson
2
在Rails 5中,uniq被移除并采用distinct代替,因此不再覆盖Ruby的本地uniq功能。 - Andrew France

37

适用于Rails 5+(Ruby 2.4.1和Postgres 9.6)

我有100个foos和9900个bars。其中99个foos每个有100个bars,而其中一个没有。

Foo.left_outer_joins(:bars).where(bars: { foo_id: nil })

生成一个 SQL 查询语句:

Foo Load (2.3ms)  SELECT  "foos".* FROM "foos" LEFT OUTER JOIN "bars" ON "bars"."foo_id" = "foos"."id" WHERE "bars"."foo_id" IS NULL

并返回没有bars的一个Foo

目前被接受的答案Foo.where.not(id: Bar.select(:foo_id).uniq)不起作用的。它会生成两个SQL查询:

Bar Load (8.4ms)  SELECT "bars"."foo_id" FROM "bars"
Foo Load (0.3ms)  SELECT  "foos".* FROM "foos" WHERE ("foos"."id" IS NOT NULL)

返回所有foos,因为所有的foos都有一个非空的id

需要更改为Foo.where.not(id: Bar.pluck(:foo_id).uniq)以将其减少为一个查询并找到我们的Foo,但在基准测试中性能较差。

require 'benchmark/ips'
require_relative 'config/environment'

Benchmark.ips do |bm|
  bm.report('left_outer_joins') do
    Foo.left_outer_joins(:bars).where(bars: { foo_id: nil })
  end

  bm.report('where.not') do
    Foo.where.not(id: Bar.pluck(:foo_id).uniq)
  end

  bm.compare!
end

Warming up --------------------------------------
    left_outer_joins     1.143k i/100ms
           where.not     6.000  i/100ms
Calculating -------------------------------------
    left_outer_joins     13.659k (± 9.0%) i/s -     68.580k in   5.071807s
           where.not     70.8569.9%) i/s -    354.000  in   5.057443s

Comparison:
    left_outer_joins:    13659.3 i/s
           where.not:       70.9 i/s - 192.77x  slower

非常感谢您的好建议! - Blue Smith
2
太棒了!您能详细说明一下“WHERE NOT EXISTS”变体吗? - systho
2
如果我理解得正确,这在Rails 6.1+中被打包为where.missing。例如:Foo.where.missing(:bars) https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing - Dom Christie
对于那些试图在多态关系中查找孤立记录的人来说,Rails(目前)不支持关系表计算,因此where.missing还不是一个选项。(“多态关联不支持计算类。”) - changingrainbows

15

在 foo.rb 文件中

class Foo < ActiveRecord::Base    
  has_many :bars
  scope :lonely, lambda { joins('LEFT OUTER JOIN bars ON foos.id = bars.foo_id').where('bars.foo_id IS NULL') }
end

由于在 Foo 上定义了 has_many :bars,我认为你可以跳过 scope 中的显式 joins(...),改用 includes,最终得到的结果是相同的: scope :lonely, lambda { includes(:bars).where('bars.foo_id': nil) - KenB

9

我更喜欢使用squeel宝石来构建复杂的查询。它通过扩展ActiveRecord实现了这样的魔法:

Foo.where{id.not_in Bar.select{foo_id}.uniq}

构建以下查询的程序:

SELECT "foos".* 
FROM "foos" 
WHERE "foos"."id" NOT IN (
  SELECT DISTINCT "bars"."foo_id"
  FROM "bars" 
)

所以,
# in Foo class
scope :lonely, where{id.not_in Bar.select{foo_id}.uniq}

这是您可以用来构建所需范围的工具。


为什么呢?这只是一个单独的SQL查询,它的嵌套部分应该非常快。 - jdoe
有没有内置的 AR 方式来实现类似的查询? - Julio G Medina
不行 :( 你必须使用文本片段来编写诸如 NOT IN 的内容,并将它们与 Ruby 代码结合起来。这种组合看起来很丑陋。比较一下:Product.where('price < ?', some_price)(AR) 和 Product.where{price < some_price}(squeel)。 - jdoe
假设有数百万条记录。您正在强制执行该表的全扫描(虽然我想数据库可能会将其重写为连接)。如果foo_id列上没有索引,则distinct也会产生影响。 - Frederick Cheung
你真的认为某种魔法连接可以消除遍历所有条的查找吗? :) - jdoe

6

使用带有LIMIT子查询的NOT EXISTS可能更快:

SELECT foos.* FROM foos
WHERE NOT EXISTS (SELECT id FROM bars WHERE bars.foo_id = foos.id LIMIT 1);

使用ActiveRecord(>= 4.0.0):

Foo.where.not(Bar.where("bars.foo_id = foos.id").limit(1).arel.exists)

0

这种方法利用includes并允许作用域链接。它应该适用于Rails 5+。

scope :barless, -> {
  includes(
    :bars
  ).where(
    bars: {
      id: nil
    }
  )
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接