ruby-on-rails - busquedas - formularios anidados rails
Desea buscar registros sin registros asociados en Rails 3 (8)
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 obtener 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 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 ...
Además, para filtrar por un amigo, por ejemplo:
Friend.where.not(id: other_friend.friends.pluck(:id))
Ambas respuestas de dmarkow y Unixmonkey me dan lo que necesito, ¡gracias!
Probé ambos en mi aplicación real y obtuve los tiempos para ellos. Estos son 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
Corrimos esto con una aplicación real - una pequeña mesa con ~ 700 registros de ''persona'' - promedio de 5 carreras
El enfoque de Unixmonkey ( :without_friends_v1
) 813ms / query
El enfoque de dmarkow ( :without_friends_v2
) 891ms / query (~ 10% más lento)
Pero luego se me ocurrió que no necesito la llamada a DISTINCT()...
Estoy buscando registros de Person
sin Contacts
, por lo que solo deben NOT IN
estar 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)") }
Eso obtiene el mismo resultado, pero con un promedio de 425 ms / llamada, casi la mitad de las veces ...
Ahora puede necesitar DISTINCT
en otras consultas similares, pero en mi caso, esto parece funcionar bien.
Gracias por tu ayuda
Desafortunadamente, probablemente esté buscando una solución que involucre SQL, pero podría establecerla en un ámbito y luego simplemente usar ese alcance:
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
Luego, para obtenerlos, puede hacer Person.without_friends
, y también puede encadenar esto con otros métodos de Arel: Person.without_friends.order("name").limit(10)
Esto sigue siendo bastante similar a SQL, pero debería hacer que todos sin amigos en el primer caso:
Person.where(''id NOT IN (SELECT DISTINCT(person_id) FROM friends)'')
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 } )
Actualizar
Tengo una pregunta sobre has_one
en los comentarios, así que solo has_one
actualizando. El truco aquí es que includes()
espera el nombre de la asociación pero el lugar where
espera el nombre de la tabla. Para un has_one
la asociación generalmente se expresará en singular, por lo que cambia, pero la parte where()
permanece como está. Entonces, si una Person
solo has_one :contact
, su declaración sería:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Actualización 2
Alguien preguntó por la inversa, 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 estar nil
así que puede ser cualquiera de ellos. Esto lleva 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 que ninguna persona se vuelva aún más simple, solo cambias 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 +1 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 para esto. ¡Gran adición!
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")
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)
El sin amigos:
Person.includes(:friends).merge(Friend.to_nobody)
Una subconsulta correlacionada NO EXISTE debe ser rápida, particularmente a medida que aumenta el recuento de filas y la proporción de registros de padres a hijos.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
smathy tiene una buena respuesta de 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 } )
Mira los documentos de API . Fue introducido en la solicitud de extracción #12071 .