Rechercher tous les enregistrements dont le nombre d'associations est supérieur à zéro

98

J'essaie de faire quelque chose que je pensais que ce serait simple mais qui ne semble pas l'être.

J'ai un modèle de projet qui a de nombreux postes vacants.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Je souhaite obtenir tous les projets qui ont au moins 1 poste vacant. J'ai essayé quelque chose comme ça:

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

mais ça dit

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
la source

Réponses:

66

joinsutilise une jointure interne par défaut, donc l'utilisation Project.joins(:vacancies)ne retournera en fait que les projets qui ont un poste vacant associé.

METTRE À JOUR:

Comme indiqué par @mackskatz dans le commentaire, sans groupclause, le code ci-dessus renverra les projets en double pour les projets avec plus d'un poste vacant. Pour supprimer les doublons, utilisez

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

METTRE À JOUR:

Comme indiqué par @Tolsee, vous pouvez également utiliser distinct.

Project.joins(:vacancies).distinct

Par exemple

[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
la source
1
Toutefois, sans appliquer une clause group by, cela renverrait plusieurs objets Project pour les projets qui ont plus d'un poste vacant.
mackshkatz
1
Cependant, ne génère pas une instruction SQL efficace.
David Aldridge
Eh bien, c'est Rails pour vous. Si vous pouvez fournir une réponse SQL (et expliquer pourquoi ce n'est pas efficace), cela peut être beaucoup plus utile.
jvnill
A quoi pensez-vous Project.joins(:vacancies).distinct?
Tolsee
1
C'est @Tolsee btw: D
Tolsee
168

1) Pour obtenir des projets avec au moins 1 poste vacant:

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

2) Pour obtenir des projets avec plus d'un poste vacant:

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

3) Ou, si le Vacancymodèle définit le cache du compteur:

belongs_to :project, counter_cache: true

alors cela fonctionnera aussi:

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

La règle d'inflexion pour vacancypeut devoir être spécifiée manuellement ?

Arta
la source
2
Cela ne devrait-il pas être le cas Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Interrogation du nombre de postes vacants au lieu des identifiants de projet
Keith Mattix
1
Non, @KeithMattix, ça ne devrait pas l'être. Cela peut être, cependant, si cela vous lit mieux; c'est une question de préférence. Le décompte peut être effectué avec n'importe quel champ de la table de jointure dont la valeur est garantie dans chaque ligne. La plupart des candidats sont significatifs projects.id, project_idet vacancies.id. J'ai choisi de compter project_idcar c'est le champ sur lequel se fait la jointure; la colonne vertébrale de la jointure si vous voulez. Cela me rappelle également qu'il s'agit d'une table de jointure.
Arta
38

Ouais, ce vacanciesn'est pas un champ dans la jointure. Je crois que tu veux:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
Peter Alfvin
la source
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
la source
5

Effectuer une jointure interne à la table has_many combinée avec un groupou uniqest potentiellement très inefficace, et en SQL, cela serait mieux implémenté comme une semi-jointure qui utilise EXISTSune sous-requête corrélée.

Cela permet à l'optimiseur de requêtes de sonder la table des postes vacants pour vérifier l'existence d'une ligne avec le project_id correct. Peu importe qu'il y ait une ligne ou un million qui aient cet id_projet.

Ce n'est pas aussi simple dans Rails, mais peut être réalisé avec:

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

De même, recherchez tous les projets qui n'ont pas de poste vacant:

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

Edit: dans les versions récentes de Rails, vous recevez un avertissement d'obsolescence vous indiquant de ne pas compter sur la existsdélégation à arel. Corrigez cela avec:

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

Edit: si vous n'êtes pas à l'aise avec le SQL brut, essayez:

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

Vous pouvez rendre cela moins compliqué en ajoutant des méthodes de classe pour masquer l'utilisation de arel_table, par exemple:

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

... alors ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)
David Aldridge
la source
ces deux suggestions ne semblent pas fonctionner ... la sous-requête Vacancy.where("vacancies.project_id = projects.id").exists?renvoie soit trueou false. Project.where(true)est un ArgumentError.
Les Nightingill
Vacancy.where("vacancies.project_id = projects.id").exists?ne va pas s'exécuter - cela déclenchera une erreur car la projectsrelation n'existera pas dans la requête (et il n'y a pas non plus de point d'interrogation dans l'exemple de code ci-dessus). Donc, décomposer cela en deux expressions n'est pas valide et ne fonctionne pas. Récemment, Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)soulève un avertissement de dépréciation ... Je vais mettre à jour la question.
David Aldridge
4

Dans Rails 4+, vous pouvez également utiliser includes ou eager_load pour obtenir la même réponse:

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

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

Je pense qu'il existe une solution plus simple:

Project.joins(:vacancies).distinct
Yuri Karpovich
la source
1
Il est également possible d'utiliser "distinct", par exemple Project.joins (: vacancies) .distinct
Metaphysiker
Vous avez raison! Il vaut mieux utiliser #distinct au lieu de #uniq. #uniq chargera tous les objets en mémoire, mais #distinct fera des calculs côté base de données.
Yuri Karpovich
3

Sans beaucoup de magie Rails, vous pouvez faire:

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

Ce type de conditions fonctionnera dans toutes les versions de Rails car une grande partie du travail est effectuée directement du côté DB. De plus, la .countméthode de chaînage fonctionnera bien aussi. J'ai été brûlé par des requêtes comme Project.joins(:vacancies)avant. Bien sûr, il y a des avantages et des inconvénients car ce n'est pas indépendant de DB.

Konyak
la source
1
C'est beaucoup plus lent que la méthode join et group, car la sous-requête 'select count (*) ..' s'exécutera pour chaque projet.
YasirAzgar
@YasirAzgar La méthode de jointure et de groupe est plus lente que la méthode "existe" car elle continuera d'accéder à toutes les lignes enfants, même s'il y en a un million.
David Aldridge
0

Vous pouvez également utiliser EXISTSavec SELECT 1plutôt que de sélectionner toutes les colonnes du vacanciestableau:

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

L'erreur vous dit que les postes vacants ne sont pas essentiellement une rubrique dans les projets.

Cela devrait fonctionner

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