Encontre todos os registros que têm uma contagem de uma associação maior que zero


98

Estou tentando fazer algo que pensei que seria simples, mas parece que não é.

Tenho um modelo de projeto que tem muitas vagas.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Quero pegar todos os projetos que tenham no mínimo 1 vaga. Eu tentei algo assim:

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

mas diz

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

Respostas:


65

joinsusa uma junção interna por padrão, portanto, o uso Project.joins(:vacancies)retornará apenas projetos que tenham uma vaga associada.

ATUALIZAR:

Conforme apontado por @mackskatz no comentário, sem groupcláusula, o código acima retornará projetos duplicados para projetos com mais de uma vaga. Para remover as duplicatas, use

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

ATUALIZAR:

Como apontado por @Tolsee, você também pode usar distinct.

Project.joins(:vacancies).distinct

Como um exemplo

[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\""

1
No entanto, sem aplicar uma cláusula group by, isso retornaria vários objetos de projeto para projetos que têm mais de uma vaga.
mackshkatz de

1
Porém, não gera uma instrução SQL eficiente.
David Aldridge

Bem, isso é Rails para você. Se você puder fornecer uma resposta sql (e explicar por que isso não é eficiente), isso pode ser muito mais útil.
jvnill

Sobre o que você pensa Project.joins(:vacancies).distinct?
Tolsee

1
É @Tolsee btw: D
Tolsee

167

1) Para conseguir Projetos com no mínimo 1 vaga:

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

2) Para obter Projetos com mais de 1 vaga:

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

3) Ou, se o Vacancymodelo definir o cache do contador:

belongs_to :project, counter_cache: true

então isso vai funcionar também:

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

A regra de inflexão para vacancypode precisar ser especificada manualmente ?


2
Não deveria ser assim Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Consultando o número de vagas, em vez das identificações de projeto
Keith Mattix

Não, @KeithMattix, não deveria ser. Ele pode ser, no entanto, se ele lê melhor para você; é uma questão de preferência. A contagem pode ser feita com qualquer campo da tabela de junção que tenha um valor garantido em cada linha. A maioria dos candidatos significativas são projects.id, project_ide vacancies.id. Optei por contar project_idporque é o campo em que a junção é feita; a espinha da junção, se quiser. Também me lembra que esta é uma mesa de junção.
Arta,

36

Sim, vacanciesnão é um campo no join. Eu acredito que você quer:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")

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')

5

A execução de uma junção interna à tabela has_many combinada com um groupou uniqé potencialmente muito ineficiente e, em SQL, seria melhor implementada como uma semi-junção que usa EXISTScom uma subconsulta correlacionada.

Isso permite que o otimizador de consulta investigue a tabela de vagas para verificar a existência de uma linha com o project_id correto. Não importa se há uma linha ou um milhão com esse project_id.

Isso não é tão simples no Rails, mas pode ser alcançado com:

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

Da mesma forma, encontre todos os projetos que não têm vagas:

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

Edit: nas versões recentes do Rails, você recebe um aviso de depreciação dizendo para não confiar em existsser delegado a arel. Corrija isso com:

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

Editar: se você não se sentir confortável com o SQL bruto, tente:

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

Você pode tornar isso menos confuso adicionando métodos de classe para ocultar o uso de arel_table, por exemplo:

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

... tão ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)

essas duas sugestões não parecem funcionar ... a subconsulta Vacancy.where("vacancies.project_id = projects.id").exists?retorna trueou false. Project.where(true)é um ArgumentError.
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?não vai ser executado - vai gerar um erro porque a projectsrelação não existirá na consulta (e também não há ponto de interrogação no código de exemplo acima). Portanto, decompor isso em duas expressões não é válido e não funciona. Recentemente, Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)levantou um aviso de depreciação ... Vou atualizar a questão.
David Aldridge,

4

No Rails 4+, você também pode usar includes ou eager_load para obter a mesma resposta:

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

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

4

Acho que há uma solução mais simples:

Project.joins(:vacancies).distinct

1
Também é possível usar "distinto", por exemplo, Project.joins (: vagas) .distinct
Metaphysiker

Você está certo! É melhor usar #distinct em vez de #uniq. #uniq carregará todos os objetos na memória, mas #distinct fará cálculos no lado do banco de dados.
Yuri Karpovich de

3

Sem muita magia Rails, você pode fazer:

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

Este tipo de condição funcionará em todas as versões do Rails já que grande parte do trabalho é feito diretamente no lado do banco de dados. Além disso, o .countmétodo de encadeamento também funcionará bem. Fui queimado por perguntas como Project.joins(:vacancies)antes. Claro, existem prós e contras, pois não é agnóstico de DB.


1
Isso é muito mais lento do que o método join e group, pois a subconsulta 'select count (*) ..' será executada para cada projeto.
YasirAzgar,

@YasirAzgar O método join e group é mais lento do que o método "exists" porque ainda acessará todas as linhas filhas, mesmo se houver um milhão delas.
David Aldridge

0

Você também pode usar EXISTScom, em SELECT 1vez de selecionar todas as colunas da vacanciestabela:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")

-6

O erro é avisar que vagas não é coluna em projetos, basicamente.

Isso deve funcionar

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

7
aggregate functions are not allowed in WHERE
Kamil Lelonek
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.