rails scope来检查关联是否不存在

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

foo.rb

class Foo < ActiveRecord::Base has_many :bars end 

bar.rb

 class Bar < ActiveRecord::Base belongs_to :foo end 

我想要一个范围,可以找到所有没有任何barsFoo's 。 很容易找到使用joins的关联,但我没有找到相反的方法。

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

在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 

适用于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因为所有foosid都不为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.143ki/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 

我更喜欢使用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} 

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

使用带有LIMIT-ed子查询的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)