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 que 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 ...
ruby-on-rails
arel
meta-where
craic.com
fuente
fuente
DISTINCT
. De lo contrario, creo que desearía normalizar los datos y el índice en ese caso. Podría hacerlo creando unfriend_ids
hstore o una columna serializada. Entonces se podría decirPerson.where(friend_ids: nil)
not exists (select person_id from friends where person_id = person.id)
(o tal vezpeople.id
opersons.id
, dependiendo de cuál sea su tabla). No estoy seguro de cuál es el más rápido en una situación particular, pero en el pasado esto me ha funcionado bien cuando no estaba tratando de usar ActiveRecord.Mejor:
Por el momento, es básicamente lo mismo, confías en el hecho de que una persona sin amigos tampoco tendrá contactos:
Actualizar
Tengo una pregunta sobre
has_one
en los comentarios, así que solo actualizando. El truco aquí es queincludes()
espera el nombre de la asociación perowhere
espera el nombre de la tabla. Para a,has_one
la asociación generalmente se expresará en singular, de modo que cambia, pero lawhere()
parte permanece como está. Entonces, siPerson
solo unahas_one :contact
, su declaración sería:Actualización 2
Alguien preguntó por el inverso, 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 sernil
así que puede ser cualquiera de ellos. Esto lleva a una solución más simple a lo anterior:Y luego cambiar esto para devolver a los amigos sin gente se vuelve aún más simple, solo cambias la clase en el frente:
Actualización 3 - Rails 5
Gracias a @Anson por la excelente solución Rails 5 (darle algunos +1 por su respuesta a continuación), puede usar
left_outer_joins
para evitar cargar la asociación:Lo he incluido aquí para que la gente lo encuentre, pero se merece los +1 por esto. Gran adición!
Actualización 4 - Rails 6.1
Gracias a Tim Park por señalar que en el próximo 6.1 puedes hacer esto:
Gracias a la publicación a la que también se vinculó.
fuente
has_one
asociación, debe cambiar el nombre de la asociación en laincludes
llamada. Entonces, suponiendo que estuvierahas_one :contact
dentro,Person
entonces su código seríaPerson.includes(:contact).where( :contacts => { :person_id => nil } )
self.table_name = "custom_friends_table_name"
), usePerson.includes(:friends).where(:custom_friends_table_name => {:id => nil})
.missing
método para hacer exactamente esto !smathy tiene una buena respuesta de Rails 3.
Para Rails 5 , puede usar
left_outer_joins
para evitar cargar la asociación.Echa un vistazo a los documentos de la API . Fue introducido en la solicitud de extracción # 12071 .
fuente
.includes
el costo adicional en tiempo de carga no sería algo de lo que me preocuparía mucho la optimización. Su caso de uso puede ser diferente.Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')
funciona bien como alcance. Hago esto todo el tiempo en mis proyectos de Rails.includes
, todos esos objetos AR se cargan en la memoria, lo que puede ser algo malo a medida que las tablas se hacen cada vez más grandes. Si no necesita acceso al registro de contacto,left_outer_joins
no carga el contacto en la memoria. La velocidad de solicitud de SQL es la misma, pero el beneficio general de la aplicación es mucho mayor.Person.where(contacts: nil)
oPerson.with(contact: contact)
si usan where invade demasiado la 'propiedad', pero dado ese contacto: ya se analiza y se identifica como una asociación, parece lógico que arel pueda resolver fácilmente lo que se requiere ...Personas que no tienen amigos.
O que tenga al menos un amigo
Puede hacer esto con Arel configurando ámbitos en
Friend
Y luego, las personas que tienen al menos un amigo:
El sin amigos:
fuente
DEPRECATION WARNING: It looks like you are eager loading table(s)
Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
Las respuestas de dmarkow y Unixmonkey me dan lo que necesito. ¡Gracias!
Probé ambos en mi aplicación real y obtuve los tiempos para ellos: aquí están los dos ámbitos:
Ejecuté esto con una aplicación real - pequeña mesa con ~ 700 registros de 'Persona' - promedio de 5 ejecuciones
Enfoque de Unixmonkey (
:without_friends_v1
) 813 ms / consultaEnfoque de dmarkow (
:without_friends_v2
) 891ms / consulta (~ 10% más lento)Pero luego se me ocurrió que no necesito la llamada para
DISTINCT()...
buscarPerson
registros con NOContacts
, por lo que solo deben serNOT IN
la lista de contactosperson_ids
. Así que probé este alcance:Eso obtiene el mismo resultado pero con un promedio de 425 ms / llamada, casi la mitad del tiempo ...
Ahora es posible que necesite
DISTINCT
otras consultas similares, pero en mi caso esto parece funcionar bien.Gracias por tu ayuda
fuente
Desafortunadamente, probablemente esté buscando una solución que involucre SQL, pero podría establecerla en un ámbito y luego simplemente usar ese ámbito:
Luego, para obtenerlos, puede hacerlo
Person.without_friends
, y también puede encadenar esto con otros métodos de Arel:Person.without_friends.order("name").limit(10)
fuente
Una subconsulta correlacionada NO EXISTE debería ser rápida, particularmente a medida que aumenta el recuento de filas y la proporción de registros de padres a hijos.
fuente
Además, para filtrar por un amigo, por ejemplo:
fuente