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

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

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 ?


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

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

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
)

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

4

Je pense qu'il existe une solution plus simple:

Project.joins(:vacancies).distinct

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.


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

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

7
aggregate functions are not allowed in WHERE
Kamil Lelonek
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.