30

我正在寻找一个范围来返回所有没有特定关联的记录。

foo.rb

class Foo < ActiveRecord::Base    
  has_many :bars
end

bar.rb

class Bar < ActiveRecord::Base    
  belongs_to :foo
end

我想要一个可以找到所有Foo's没有范围bars。使用 很容易找到具有关联的那些joins,但我还没有找到相反的方法。

4

6 回答 6

46

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) }
于 2014-04-30T13:14:35.020 回答
30

对于 Rails 5+(Ruby 2.4.1 和 Postgres 9.6)

我有 100foos和 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

并返回Foo没有的bars

当前接受的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.856  (± 9.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
于 2017-08-08T17:11:19.023 回答
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
于 2013-01-10T09:54:24.110 回答
9

我更喜欢使用squeel gem 来构建复杂的查询。它以这样的魔力扩展了 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}

是您可以用来构建请求范围的内容。

于 2012-04-27T18:02:24.023 回答
6

将 NOT EXISTS 与 LIMIT-ed 子查询一起使用会更快:

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)
于 2014-07-07T10:22:18.867 回答
0

此方法利用includes并允许范围链接。它应该适用于 Rails 5+

scope :barless, -> {
  includes(
    :bars
  ).where(
    bars: {
      id: nil
    }
  )
}
于 2020-09-22T20:31:57.020 回答