Vous voulez trouver des enregistrements sans enregistrements associés dans Rails 3

Considérons une association simple …

class Person has_many :friends end class Friend belongs_to :person end 

Quelle est la manière la plus propre d’obtenir toutes les personnes qui n’ont AUCUN ami dans ARel et / ou meta_where?

Et puis qu’en est-il de has_many: par la version

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true end class Friend has_many :contacts has_many :people, :through => :contacts, :uniq => true end class Contact belongs_to :friend belongs_to :person end 

Je ne veux vraiment pas utiliser counter_cache – et d’après ce que j’ai lu, cela ne fonctionne pas avec has_many: through

Je ne veux pas extraire tous les enregistrements de person.friends et les parcourir en Ruby – Je veux avoir une requête / une scope que je peux utiliser avec le joyau de meta_search

Le coût de performance des requêtes ne me dérange pas

Et plus on s’éloigne du SQL réel, mieux c’est …

Ceci est encore assez proche de SQL, mais il devrait y avoir tout le monde sans amis dans le premier cas:

 Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)') 

Meilleur:

 Person.includes(:friends).where( :friends => { :person_id => nil } ) 

Pour le hmt c’est fondamentalement la même chose, vous vous fiez au fait qu’une personne sans amis n’aura pas de contacts:

 Person.includes(:contacts).where( :contacts => { :person_id => nil } ) 

Mettre à jour

Vous avez une question sur has_one dans les commentaires, donc juste une mise à jour. L’astuce ici est que includes() attend le nom de l’association mais where attend le nom de la table. Pour un has_one l’association sera généralement exprimée au singulier, ce qui change, mais la partie where() rest telle quelle. Donc, si une seule Person has_one :contact alors votre déclaration serait:

 Person.includes(:contact).where( :contacts => { :person_id => nil } ) 

Mise à jour 2

Quelqu’un a demandé l’inverse, des amis sans personnes. Comme je l’ai indiqué ci-dessous, cela m’a fait réaliser que le dernier champ (ci-dessus: le :person_id ) ne doit pas nécessairement être lié au modèle que vous retournez, il doit simplement être un champ dans la table de jointure. Ils vont tous être nil pour pouvoir être l’un d’entre eux. Cela conduit à une solution plus simple à ce qui précède:

 Person.includes(:contacts).where( :contacts => { :id => nil } ) 

Et puis changer cela pour rendre les amis sans personne devient encore plus simple, vous ne changez que la classe à l’avant:

 Friend.includes(:contacts).where( :contacts => { :id => nil } ) 

Mise à jour 3 – Rails 5

Grâce à @Anson pour l’excellente solution Rails 5 (donnez-lui quelques +1 pour sa réponse ci-dessous), vous pouvez utiliser left_outer_joins pour éviter de charger l’association:

 Person.left_outer_joins(:contacts).where( contacts: { id: nil } ) 

Je l’ai inclus ici pour que les gens le trouvent, mais il mérite les + 1 pour cela. Excellent ajout!

Smathy a une bonne réponse Rails 3.

Pour Rails 5 , vous pouvez utiliser left_outer_joins pour éviter de charger l’association.

 Person.left_outer_joins(:contacts).where( contacts: { id: nil } ) 

Découvrez les docs api . Il a été introduit dans la requête pull # 12071 .

Personnes qui n’ont pas d’amis

 Person.includes(:friends).where("friends.person_id IS NULL") 

Ou qui ont au moins un ami

 Person.includes(:friends).where("friends.person_id IS NOT NULL") 

Vous pouvez le faire avec Arel en mettant en place des étendues sur Friend

 class Friend belongs_to :person scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) } scope :to_nobody, ->{ where arel_table[:person_id].eq(nil) } end 

Et puis, les personnes qui ont au moins un ami:

 Person.includes(:friends).merge(Friend.to_somebody) 

Le sans ami:

 Person.includes(:friends).merge(Friend.to_nobody) 

Les réponses de dmarkow et d’Unixmonkey me donnent tout ce dont j’ai besoin – Merci!

J’ai essayé les deux dans ma vraie application et j’ai obtenu des timings pour eux – Voici les deux champs d’application:

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") } scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") } end 

A couru ceci avec une vraie application – petite table avec ~ 700 dossiers ‘Person’ – moyenne de 5 courses

Approche d’Unixmonkey ( :without_friends_v1 without_friends_v1) 813ms / query

L’approche de dmarkow ( :without_friends_v2 without_friends_v2) 891ms / query (~ 10% plus lent)

Mais il m’est apparu que je n’avais pas besoin de l’appel à DISTINCT()... Je recherche des enregistrements de Person avec NO Contacts – il suffit donc qu’ils ne soient NOT IN la liste des contacts person_ids . J’ai donc essayé cette scope:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") } 

Cela donne le même résultat mais avec une moyenne de 425 ms / appel – presque la moitié du temps …

Maintenant, vous pourriez avoir besoin du DISTINCT dans d’autres requêtes similaires – mais dans mon cas, cela semble fonctionner correctement.

Merci de votre aide

Malheureusement, vous recherchez probablement une solution impliquant SQL, mais vous pouvez la définir dans une étendue, puis utiliser simplement cette étendue:

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0") end 

Ensuite, pour les obtenir, vous pouvez simplement faire Person.without_friends , et vous pouvez aussi les enchaîner avec d’autres méthodes Arel: Person.without_friends.order("name").limit(10)

Une sous-requête corrélée NOT EXISTS doit être rapide, en particulier lorsque le nombre de lignes et le ratio des enregistrements enfant / parent augmentent.

 scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)") 

Aussi, pour filtrer par un ami par exemple:

 Friend.where.not(id: other_friend.friends.pluck(:id))