rails joins inverse_of foreign belong association sql ruby-on-rails associations scopes

sql - joins - relationship rails 5



Rieles de alcance para comprobar si la asociaciĆ³n NO existe. (5)

Para Rails 5+ (Ruby 2.4.1 y Postgres 9.6)

Tengo 100 foos y 9900 bars . 99 de los foos tienen 100 bars cada uno, y uno de ellos no tiene ninguno.

Foo.left_outer_joins(:bars).where(bars: { foo_id: nil })

Produce una consulta 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

y devuelve el de Foo sin bars

La respuesta actualmente aceptada Foo.where.not(id: Bar.select(:foo_id).uniq) no está funcionando. Está produciendo dos consultas 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)

que devuelve todos los foos porque todos los foos tienen un id que no es nulo.

Se debe cambiar a Foo.where.not(id: Bar.pluck(:foo_id).uniq) para reducirlo a una consulta y encontrar nuestro Foo , pero tiene un bajo rendimiento en los puntos de referencia

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

Estoy buscando escribir un alcance que devuelva todos los registros que no tengan una asociación en particular.

foo.rb

class Foo < ActiveRecord::Base has_many :bars end

bar.rb

class Bar < ActiveRecord::Base belongs_to :foo end

Quiero un alcance que pueda encontrar todos los Foo''s que no tengan bars . Es fácil encontrar a los que tienen una asociación mediante joins , pero no he encontrado la manera de hacer lo contrario.


Prefiero usar gema squeel para construir consultas complejas. Extiende ActiveRecord con tal magia:

Foo.where{id.not_in Bar.select{foo_id}.uniq}

que construye la siguiente consulta:

SELECT "foos".* FROM "foos" WHERE "foos"."id" NOT IN ( SELECT DISTINCT "bars"."foo_id" FROM "bars" )

Asi que,

# in Foo class scope :lonely, where{id.not_in Bar.select{foo_id}.uniq}

Es lo que puedes usar para construir el alcance solicitado.


Rails 4 hace esto muy fácil :)

Foo.where.not(id: Bar.select(:foo_id).uniq)

Esto genera la misma consulta que la respuesta de Jdoe.

SELECT "foos".* FROM "foos" WHERE "foos"."id" NOT IN ( SELECT DISTINCT "bars"."foo_id" FROM "bars" )

Y como alcance:

scope :lonely, -> { where.not(id: Bar.select(:item_id).uniq) }


Usar NOT EXISTS con una subconsulta LIMITADA puede ser más rápido:

SELECT foos.* FROM foos WHERE NOT EXISTS (SELECT id FROM bars WHERE bars.foo_id = foos.id LIMIT 1);

Con ActiveRecord (> = 4.0.0):

Foo.where.not(Bar.where("bars.foo_id = foos.id").limit(1).arel.exists)


en 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