Quiere buscar registros sin registros asociados en Rails
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...
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_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!
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 nil
así 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_one
en los comentarios, así que estoy actualizando. El truco aquí es que includes()
espera el nombre de la asociación pero where
espera el nombre de la tabla. Para a, has_one
la asociación generalmente se expresará en singular, por lo que eso cambia, pero la where()
parte permanece como está. Entonces, si Person
solo has_one :contact
entonces 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 tiene una buena respuesta de Rails 3.
Para Rails 5 , puede utilizar left_outer_joins
para 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 .
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)')
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)
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 Person
registros con NO Contacts
, por lo que solo deben ser NOT IN
la 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 DISTINCT
en otras consultas similares, pero en mi caso esto parece funcionar bien.
Gracias por tu ayuda