Quiere buscar registros sin registros asociados en Rails

Resuelto craic.com asked hace 13 años • 9 respuestas

Considere una asociación simple...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

¿Cuál es la forma más limpia de conseguir a todas las personas que NO tienen amigos en ARel y/o meta_where?

Y luego, ¿qué pasa con una versión has_many:through?

class Person
   has_many :contacts
   has_many :friends, :through => :contacts, :uniq => true
end

class Friend
   has_many :contacts
   has_many :people, :through => :contacts, :uniq => true
end

class Contact
   belongs_to :friend
   belongs_to :person
end

Realmente no quiero usar counter_cache y, por lo que he leído, no funciona con has_many :through

No quiero extraer todos los registros de person.friends y recorrerlos en Ruby. Quiero tener una consulta/alcance que pueda usar con la gema meta_search

No me importa el costo de rendimiento de las consultas.

Y cuanto más lejos del SQL real, mejor...

craic.com avatar Mar 16 '11 06:03 craic.com
Aceptado

Actualización 4 - Rieles 6.1

Gracias a Tim Park por señalar que en la próxima versión 6.1 puedes hacer esto:

Person.where.missing(:contacts)

Gracias a la publicación a la que se vinculó también.

Actualización 3 - Rieles 5

Gracias a @Anson por la excelente solución Rails 5 (dale algunos +1 por su respuesta a continuación), puedes usarla left_outer_joinspara evitar cargar la asociación:

Person.left_outer_joins(:contacts).where(contacts: { id: nil })

Lo he incluido aquí para que la gente lo encuentre, pero se merece los +1 por esto. ¡Gran adición!

Actualización 2

Alguien preguntó por lo contrario, amigos sin gente. Como comenté a continuación, esto me hizo darme cuenta de que el último campo (arriba: el :person_id) en realidad no tiene que estar relacionado con el modelo que estás devolviendo, solo tiene que ser un campo en la tabla de combinación. Todos van a ser nilasí que puede ser cualquiera de ellos. Esto lleva a una solución más sencilla a lo anterior:

Person.includes(:contacts).where(contacts: { id: nil })

Y luego cambiar esto para devolver los amigos sin personas se vuelve aún más simple, solo cambias la clase al frente:

Friend.includes(:contacts).where(contacts: { id: nil })

Actualizar

Tengo una pregunta has_oneen los comentarios, así que estoy actualizando. El truco aquí es que includes()espera el nombre de la asociación pero whereespera el nombre de la tabla. Para a, has_onela asociación generalmente se expresará en singular, por lo que eso cambia, pero la where()parte permanece como está. Entonces, si Personsolo has_one :contactentonces su declaración sería:

Person.includes(:contact).where(contacts: { person_id: nil })

Original

Mejor:

Person.includes(:friends).where(friends: { person_id: nil })

Para el hmt es básicamente lo mismo, confías en el hecho de que una persona sin amigos tampoco tendrá contactos:

Person.includes(:contacts).where(contacts: { person_id: nil })
smathy avatar Apr 06 '2011 17:04 smathy

smathy tiene una buena respuesta de Rails 3.

Para Rails 5 , puede utilizar left_outer_joinspara evitar cargar la asociación.

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

Consulte los documentos de la API . Se introdujo en la solicitud de extracción n.° 12071 .

Anson avatar Nov 09 '2016 16:11 Anson

Esto todavía está bastante cerca de SQL, pero debería hacer que todos los que no tienen amigos en el primer caso:

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Unixmonkey avatar Mar 16 '2011 00:03 Unixmonkey

personas que no tienen amigos

Person.includes(:friends).where("friends.person_id IS NULL")

O que tenga al menos un amigo

Person.includes(:friends).where("friends.person_id IS NOT NULL")

Puedes hacer esto con Arel configurando ámbitos enFriend

class Friend
  belongs_to :person

  scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
  scope :to_nobody,   ->{ where arel_table[:person_id].eq(nil) }
end

Y luego, Personas que tienen al menos un amigo:

Person.includes(:friends).merge(Friend.to_somebody)

Los sin amigos:

Person.includes(:friends).merge(Friend.to_nobody)
novemberkilo avatar Sep 29 '2013 16:09 novemberkilo

Tanto las respuestas de dmarkow como de Unixmonkey me brindan lo que necesito. ¡Gracias!

Probé ambos en mi aplicación real y obtuve tiempos para ellos. Aquí están los dos alcances:

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
  scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end

Ejecuté esto con una aplicación real - tabla pequeña con ~700 registros de 'Persona' - promedio de 5 ejecuciones

Enfoque de Unixmonkey ( :without_friends_v1) 813 ms/consulta

Enfoque de dmarkow ( :without_friends_v2) 891 ms/consulta (~ 10% más lento)

Pero luego se me ocurrió que no necesito la llamada. DISTINCT()...Estoy buscando Personregistros con NO Contacts, por lo que solo deben ser NOT INla lista de contactos person_ids. Entonces probé este alcance:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }

Se obtiene el mismo resultado pero con un promedio de 425 ms/llamada, casi la mitad del tiempo...

Ahora es posible que necesites el DISTINCTen otras consultas similares, pero en mi caso esto parece funcionar bien.

Gracias por tu ayuda

craic.com avatar Mar 16 '2011 15:03 craic.com