rails formularios busquedas anidados ruby-on-rails arel meta-where

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 .