Desea encontrar registros sin registros asociados en Rails 3


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 un has_many: a través de la versión

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: a través de

No quiero tirar de toda la persona.registros de amigos y loop a través de ellos en Ruby-Quiero tener una consulta / ámbito que pueda utilizar con el meta_search gem

No me importa el costo de rendimiento de las consultas

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

Author: craic.com, 2011-03-16

8 answers

Esto sigue siendo bastante cercano a SQL, pero debería tener a todos los que no tengan amigos en el primer caso:

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
 87
Author: Unixmonkey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-16 15:22:42

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

Actualización

Tengo una pregunta sobre has_one en los comentarios, así que simplemente actualizando. El truco aquí es que includes() espera el nombre de la asociación, pero el where espera el nombre de la tabla. Para un has_one la asociación generalmente se expresa en singular, de modo que cambia, pero la parte where() permanece como está. Así que si un Person solo has_one :contact entonces su declaración sería:

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

Actualización 2

Alguien preguntó por lo contrario, amigos sin gente. Como comenté a continuación, esto realmente 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á devolviendo, solo tiene que ser un campo en la tabla de unión. Todos van a ser nil así que puede ser cualquiera de ellos. Esto conduce a una solución más simple a lo anterior:

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

Y luego cambiar esto para devolver a los amigos sin gente se vuelve aún más simple, solo cambia la clase en el frente:

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

Actualización 3-Rails 5

Gracias a @Anson por la excelente solución Rails 5 (dale algunos +1s por su respuesta a continuación), puedes usar left_outer_joins para 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!

 360
Author: smathy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-07 00:54:24

Smathy tiene una buena respuesta a Rails 3.

Para Rails 5 , puede usar left_outer_joins para evitar cargar la asociación.

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

Echa un vistazo a los documentos de la api . Fue introducido en pull request #12071.

 102
Author: Anson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-09 16:19:01

Personas que no tienen amigos

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

O que tienen al menos un amigo

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

Puede hacer esto con Arel configurando ámbitos en Friend

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, Las Personas que tienen al menos un amigo:

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

Los sin amigos:

Person.includes(:friends).merge(Friend.to_nobody)
 12
Author: novemberkilo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-29 16:05:09

Ambas respuestas de dmarkow y Unixmonkey me dan lo que necesito - ¡Gracias!

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

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

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

Enfoque de Unixmonkey (:without_friends_v1) 813ms / query

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

Pero entonces se me ocurrió que no necesito la llamada a DISTINCT()... Estoy buscando Person registros sin Contacts - por lo que solo tienen que ser NOT IN la lista de contactos person_ids. Así que probé este alcance:

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

Que obtiene el mismo resultado pero con un promedio de 425 ms/call - casi la mitad del tiempo...

Ahora es posible que necesite el DISTINCT en otras consultas similares, pero para mi caso esto parece funcionar bien.

Gracias por su ayuda

 10
Author: craic.com,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-11 19:02:30

Desafortunadamente, probablemente esté buscando una solución que involucre SQL, pero podría configurarla en un ámbito y luego simplemente usar ese ámbito:

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end

Entonces para obtenerlos, puedes simplemente hacer Person.without_friends, y también puedes encadenar esto con otros métodos de Arel: Person.without_friends.order("name").limit(10)

 5
Author: Dylan Markow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-16 00:29:54

Una subconsulta correlacionada NO EXISTE debe ser rápida, particularmente a medida que aumenta el número de filas y la proporción de registros hijo a padre.

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
 1
Author: David Aldridge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-16 08:40:28

También, para filtrar por un amigo, por ejemplo:

Friend.where.not(id: other_friend.friends.pluck(:id))
 1
Author: Dorian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 23:53:13