web-dev-qa-db-fra.com

Vous voulez trouver des enregistrements sans enregistrement associé dans Rails

Considérons une simple association ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

Quel est le moyen le plus propre d’attirer toutes les personnes qui n’ont AUCUN ami dans ARel et/ou meta_where?

Et puis que dire de has_many: à travers 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: grâce à

Je ne veux pas extraire tous les enregistrements person.friends et les parcourir en boucle dans Ruby - je souhaite disposer d'une requête/d'une portée que je puisse utiliser avec le gem meta_search

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

Et plus on s'éloigne de SQL, mieux c'est ...

165
craic.com

Cela reste assez proche de SQL, mais cela devrait amener tout le monde sans amis dans le premier cas:

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

Mieux:

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

Pour le hmt, c'est fondamentalement la même chose, vous vous appuyez sur le fait qu'une personne sans amis n'aura également aucun contact:

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

Mise à jour

Vous avez une question à propos de has_one Dans les commentaires, alors il suffit de mettre à jour. Le truc ici est que includes() attend le nom de l'association mais le where attend le nom de la table. Pour un has_one, L'association sera généralement exprimée au singulier, ce qui changera, mais la partie where() restera telle quelle. Donc, si un Person seulement has_one :contact, Votre déclaration serait:

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

Mise à jour 2

Quelqu'un a posé des questions sur l'inverse, des amis sans personnes. Comme je l'ai commenté ci-dessous, cela m'a fait comprendre 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 s'agir d'un champ dans le champ. rejoindre la table. Ils vont tous être nil donc ça peut être n'importe lequel d'entre eux. Cela conduit à une solution plus simple à ce qui précède:

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

Et puis changer pour renvoyer 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

Merci à @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!

405
smathy

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 le api docs . Il a été introduit dans une requête pull # 12071 .

143
Anson

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

Les sans amis:

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

Les réponses de dmarkow et d'Unixmonkey m'apportent ce dont j'ai besoin - Merci!

J'ai essayé les deux dans ma vraie application et obtenu des timings pour eux - Voici les deux portées:

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 cela avec une vraie application - petite table avec ~ 700 enregistrements 'Personne' - moyenne de 5 analyses

L'approche d'Unixmonkey (:without_friends_v1) 813ms/query

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

Mais alors, je me suis dit que je n'avais pas besoin d'appeler DISTINCT().... Je cherche Person enregistrements avec NO Contacts. Ils doivent donc être NOT IN La liste de contacts person_ids. J'ai donc essayé cette portée:

  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 aurez peut-être besoin de DISTINCT dans d'autres requêtes similaires - mais dans mon cas, cela semble fonctionner correctement.

Merci de votre aide

11
craic.com

Malheureusement, vous recherchez probablement une solution impliquant SQL, mais vous pouvez le définir dans une portée et ensuite simplement utiliser cette portée:

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 chaîner cela avec d'autres méthodes Arel: Person.without_friends.order("name").limit(10)

5
Dylan Markow

Une sous-requête corrélée NOT EXISTS doit être rapide, en particulier à mesure que le nombre de lignes et le rapport d'enregistrements enfant sur parent augmentent.

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

En outre, pour filtrer par un ami par exemple:

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