Encuentre todos los registros que tengan un recuento de una asociación mayor que cero

98

Estoy tratando de hacer algo que pensé que sería simple, pero parece que no lo es.

Tengo un modelo de proyecto que tiene muchas vacantes.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Quiero conseguir todos los proyectos que tengan al menos 1 vacante. Intenté algo como esto:

Project.joins(:vacancies).where('count(vacancies) > 0')

pero dice

SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).

jphorta
fuente

Respuestas:

65

joinsusa una combinación interna de forma predeterminada, por lo que el uso Project.joins(:vacancies)solo devolverá los proyectos que tengan una vacante asociada.

ACTUALIZAR:

Como señaló @mackskatz en el comentario, sin una groupcláusula, el código anterior devolverá proyectos duplicados para proyectos con más de una vacante. Para eliminar los duplicados, use

Project.joins(:vacancies).group('projects.id')

ACTUALIZAR:

Como lo señaló @Tolsee, también puede usar distinct.

Project.joins(:vacancies).distinct

Como ejemplo

[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""
jvnill
fuente
1
Sin embargo, sin aplicar una cláusula group by, esto devolvería varios objetos Project para proyectos que tienen más de una vacante.
mackshkatz
1
Sin embargo, no genera una declaración SQL eficiente.
David Aldridge
Bueno, eso es Rails para ti. Si puede proporcionar una respuesta SQL (y explicar por qué esto no es eficiente), puede ser mucho más útil.
jvnill
¿Qué opinas sobre Project.joins(:vacancies).distinct?
Tolsee
1
Es @Tolsee por cierto: D
Tolsee
167

1) Para conseguir proyectos con al menos 1 vacante:

Project.joins(:vacancies).group('projects.id')

2) Para obtener proyectos con más de 1 vacante:

Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')

3) O, si el Vacancymodelo establece la caché del contador:

belongs_to :project, counter_cache: true

entonces esto también funcionará:

Project.where('vacancies_count > ?', 1)

¿Es vacancyposible que sea necesario especificar manualmente la regla de inflexión para ?

Arta
fuente
2
¿No debería ser esto Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Consultando el número de vacantes en lugar de los
ID
No, @KeithMattix, no debería ser así. Sin embargo, puede serlo si le parece mejor; es una cuestión de preferencia. El recuento se puede realizar con cualquier campo de la tabla de combinación que tenga un valor garantizado en cada fila. La mayoría de los candidatos son significativas projects.id, project_idy vacancies.id. Elegí contar project_idporque es el campo en el que se realiza la unión; la columna vertebral de la unión si se quiere. También me recuerda que esta es una tabla de combinación.
Arta
36

Sí, vacanciesno es un campo en la combinación. Creo que quieres:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
Peter Alfvin
fuente
16
# None
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')
dorio
fuente
5

Realizar una unión interna a la tabla has_many combinada con un groupo uniqes potencialmente muy ineficiente, y en SQL esto se implementaría mejor como una semi-unión que se usa EXISTScon una subconsulta correlacionada.

Esto permite al optimizador de consultas sondear la tabla de vacantes para verificar la existencia de una fila con el project_id correcto. No importa si hay una fila o un millón que tienen ese project_id.

Eso no es tan sencillo en Rails, pero se puede lograr con:

Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)

Del mismo modo, busque todos los proyectos que no tengan vacantes:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)

Editar: en las versiones recientes de Rails, recibe una advertencia de desaprobación que le indica que no dependa de existsser delegado a arel. Arregle esto con:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)

Editar: si no se siente cómodo con SQL sin formato, intente:

Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)

Puede hacer que esto sea menos complicado agregando métodos de clase para ocultar el uso de arel_table, por ejemplo:

class Project
  def self.id_column
    arel_table[:id]
  end
end

... entonces ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)
David Aldridge
fuente
estas dos sugerencias no parecen funcionar ... la subconsulta Vacancy.where("vacancies.project_id = projects.id").exists?produce trueo false. Project.where(true)es un ArgumentError.
Les Nightingill
Vacancy.where("vacancies.project_id = projects.id").exists?no se ejecutará; generará un error porque la projectsrelación no existirá en la consulta (y tampoco hay un signo de interrogación en el código de muestra anterior). Entonces, descomponer esto en dos expresiones no es válido y no funciona. Recientemente, Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)plantea una advertencia de desaprobación ... Actualizaré la pregunta.
David Aldridge
4

En Rails 4+, también puedes usar includes o eager_load para obtener la misma respuesta:

Project.includes(:vacancies).references(:vacancies).
        where.not(vacancies: {id: nil})

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})
Konyak
fuente
4

Creo que hay una solución más sencilla:

Project.joins(:vacancies).distinct
Yuri Karpovich
fuente
1
También es posible utilizar "distinto", por ejemplo, Project.joins (: vacantes) .distinct
Metaphysiker
¡Tienes razón! Es mejor usar #distinct en lugar de #uniq. #uniq cargará todos los objetos en la memoria, pero #distinct hará cálculos en el lado de la base de datos.
Yuri Karpovich
3

Sin mucha magia de Rails, puedes hacer:

Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')

Este tipo de condiciones funcionará en todas las versiones de Rails ya que gran parte del trabajo se realiza directamente en el lado de la base de datos. Además, el .countmétodo de encadenamiento también funcionará bien. Me han quemado consultas como Project.joins(:vacancies)antes. Por supuesto, existen pros y contras, ya que no es independiente de DB.

Konyak
fuente
1
Esto es mucho más lento que el método join y group, ya que la subconsulta 'select count (*) ..' se ejecutará para cada proyecto.
YasirAzgar
@YasirAzgar El método de unión y grupo es más lento que el método "existe" porque aún accederá a todas las filas secundarias, incluso si hay un millón de ellas.
David Aldridge
0

También puede usar EXISTScon en SELECT 1lugar de seleccionar todas las columnas de la vacanciestabla:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
KM Rakibul Islam
fuente
-6

El error es decirte que las vacantes no es una columna en los proyectos, básicamente.

Esto debería funcionar

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')
wkhatch
fuente
7
aggregate functions are not allowed in WHERE
Kamil Lelonek