rails范围以检查关联是否不存在

Jul*_*ina 23 sql ruby-on-rails associations scopes

我期待编写一个范围,返回所有没有特定关联的记录.

foo.rb

class Foo < ActiveRecord::Base    
  has_many :bars
end
Run Code Online (Sandbox Code Playgroud)

bar.rb

class Bar < ActiveRecord::Base    
  belongs_to :foo
end
Run Code Online (Sandbox Code Playgroud)

我希望有一个范围,可以找到所有的Foo's不要有任何bars.很容易找到使用关联的那些joins,但我还没有找到相反的方法.

dav*_*son 39

Rails 4让这太容易:)

Foo.where.not(id: Bar.select(:foo_id).uniq)
Run Code Online (Sandbox Code Playgroud)

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

SELECT "foos".* 
FROM "foos" 
WHERE "foos"."id" NOT IN (
  SELECT DISTINCT "bars"."foo_id"
  FROM "bars" 
)
Run Code Online (Sandbox Code Playgroud)

作为范围:

scope :lonely, -> { where.not(id: Bar.select(:item_id).uniq) }
Run Code Online (Sandbox Code Playgroud)

  • 是的,而 `.select` 返回一个 ActiveRecord Relation,而 `.pluck` 返回一个 Ruby 数组。但是当使用`.select` 作为子查询添加时,你只调用数据库一次,因为 rails 会自动将它与其他查询结合起来。使用`.pluck`,你会调用数据库两次。另外,请记住,作用域通常是链接在一起的,因此*不建议*使用`.pluck`。 (2认同)
  • 在 Rails 5 中,`uniq` 被移除,取而代之的是 `distinct`,因此它不再覆盖 Ruby 的原生 `uniq`。 (2认同)

m. *_*org 20

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

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

Foo.left_outer_joins(:bars).where(bars: { foo_id: nil })
Run Code Online (Sandbox Code Playgroud)

生成一个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
Run Code Online (Sandbox Code Playgroud)

并返回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)
Run Code Online (Sandbox Code Playgroud)

返回all,foos因为所有foos都有一个id非null.

需要将其更改为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
Run Code Online (Sandbox Code Playgroud)

  • 大提示!您能否详细说明“不存在”的变体? (2认同)
  • 如果我理解正确的话,这在 Rails 6.1+ 中已被打包为“where.missing”。例如 `Foo.where.missing(:bars)` https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing (2认同)

shw*_*eta 13

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


jdo*_*doe 9

我更喜欢使用squeel gem来构建复杂的查询.它以如此神奇的方式扩展了ActiveRecord:

Foo.where{id.not_in Bar.select{foo_id}.uniq}
Run Code Online (Sandbox Code Playgroud)

构建以下查询:

SELECT "foos".* 
FROM "foos" 
WHERE "foos"."id" NOT IN (
  SELECT DISTINCT "bars"."foo_id"
  FROM "bars" 
)
Run Code Online (Sandbox Code Playgroud)

所以,

# in Foo class
scope :lonely, where{id.not_in Bar.select{foo_id}.uniq}
Run Code Online (Sandbox Code Playgroud)

是您可以用来构建请求的范围.


Sjo*_*erd 5

使用NOT EXISTS和LIMIT-ed子查询可以更快:

SELECT foos.* FROM foos
WHERE NOT EXISTS (SELECT id FROM bars WHERE bars.foo_id = foos.id LIMIT 1);
Run Code Online (Sandbox Code Playgroud)

使用ActiveRecord(> = 4.0.0):

Foo.where.not(Bar.where("bars.foo_id = foos.id").limit(1).arel.exists)
Run Code Online (Sandbox Code Playgroud)