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

jphorta
fonte

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\""
jvnill
fonte
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 ?

Arta
fonte
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")
Peter Alfvin
fonte
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')
Dorian
fonte
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
)
David Aldridge
fonte
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})
konyak
fonte
4

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

Project.joins(:vacancies).distinct
Yuri Karpovich
fonte
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.

konyak
fonte
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)")
KM Rakibul Islam
fonte
-6

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

Isso deve funcionar

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