Quiere encontrar registros sin registros asociados en Rails
-
24-10-2019 - |
Pregunta
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 contra_cache, y de lo que he leído, no funciona con has_many: a través de
No quiero tirar de todos los registros de la persona.
No me importa el costo de rendimiento de las consultas
Y cuanto más lejos del sql real, mejor ...
Solución
Esto todavía está bastante cerca de SQL, pero debería hacer que todos sin amigos en el primer caso:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Otros consejos
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 actualiza. El truco aquí es que includes()
espera el nombre de la asociación pero el where
espera el nombre de la tabla. Para has_one
La asociación generalmente se expresará en el singular, de modo que cambia, pero el where()
Parte se queda como es. Entonces si un Person
solamente has_one :contact
Entonces tu declaración sería:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Actualización 2
Alguien preguntó por lo inverso, amigos sin gente. Como comenté a continuación, esto en realidad 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
Entonces 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 personas se vuelve aún más simple, cambias solo 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 para 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 merece los +1 para esto. ¡Gran adición!
Smathy tiene una buena respuesta de Rails 3.
Para rieles 5, puedes usar left_outer_joins
para evitar cargar la asociación.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Revisar la documentos de API. Se introdujo en la solicitud de extracción #12071.
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")
Puedes 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, personas que tienen al menos un amigo:
Person.includes(:friends).merge(Friend.to_somebody)
El sin amigos:
Person.includes(:friends).merge(Friend.to_nobody)
Tanto las respuestas de Dmarkow como UnixMonkey me dan lo que necesito, ¡gracias!
Probé ambos en mi aplicación real y obtuve horarios 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
Ejecutó esto con una aplicación real: mesa pequeña con ~ 700 'persona' registros - promedio de 5 carreras
Enfoque de UnixMonkey (:without_friends_v1
) 813ms / consulta
enfoque de dmarkow (:without_friends_v2
) 891ms / consulta (~ 10% más lento)
Pero luego se me ocurrió que no necesito la llamada a DISTINCT()...
Estoy buscando Person
Registros con no Contacts
- Entonces solo necesitan ser NOT IN
la lista de contacto 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 del tiempo ...
Ahora es posible que necesite el DISTINCT
En otras consultas similares, pero para mi caso esto parece funcionar bien.
Gracias por tu ayuda
Desafortunadamente, probablemente esté buscando una solución que involucre SQL, pero podría configurarla en un alcance y luego 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 conseguirlos, puedes hacer Person.without_friends
, y también puedes encadenar esto con otros métodos Arel: Person.without_friends.order("name").limit(10)
A No existe que la subconsulta correlacionada debería ser rápida, particularmente a medida que aumenta el recuento de filas y la relación de los registros de niños a los padres.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Además, para filtrar por un amigo, por ejemplo:
Friend.where.not(id: other_friend.friends.pluck(:id))