Подтвердить что ты не робот

Rails, чтобы проверить, не существует ли ассоциация

Я ищу запись области, которая возвращает все записи, которые не имеют конкретной ассоциации.

foo.rb

class Foo < ActiveRecord::Base    
  has_many :bars
end

bar.rb

class Bar < ActiveRecord::Base    
  belongs_to :foo
end

Я хочу область, в которой можно найти все Foo's, которые не имеют bars. Легко найти те, которые имеют ассоциацию, используя joins, но я не нашел способ сделать обратное.

4b9b3361

Ответ 1

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

Ответ 2

в 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

Ответ 3

Я предпочитаю использовать 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}

- это то, что вы можете использовать для создания запрошенной области.

Ответ 4

Для 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

и возвращает значение 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

Ответ 5

Использование 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)