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 ...

¿Fue útil?

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))
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top