rails scope来检查关联是否不存在
我期待编写一个范围,返回所有没有特定关联的记录。
foo.rb
class Foo < ActiveRecord::Base has_many :bars end
bar.rb
class Bar < ActiveRecord::Base belongs_to :foo end
我想要一个范围,可以找到所有没有任何bars
的Foo'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
因为所有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.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)