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...
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)')
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!
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.
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)
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
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)
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)")
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))
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