211 votes

Voulez trouver des enregistrements sans enregistrements associés dans Rails 3

Envisager une simple association de fait...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

Quelle est la façon la plus propre à obtenir toutes les personnes qui ont PAS d'amis dans ARel et/ou meta_where?

Et puis que dire d'un has_many :jusqu'à 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 moi de ce que j'ai lu il ne fonctionne pas avec has_many :through

Je ne veux pas tirer toute la personne.amis des enregistrements et de les parcourir en boucle dans Ruby - je souhaite faire une requête/champ d'application que je peux utiliser avec le meta_search gem

Je n'ai pas l'esprit le coût de performances des requêtes

Et le plus loin de SQL réel le mieux...

524voto

smathy Points 6925

Mieux:

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

Pour la hmt c'est fondamentalement la même chose, vous pouvez compter sur le fait qu'une personne n'ayant pas d'amis aussi ne pas avoir de contacts:

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

Mise à jour

Vous avez une question à propos de has_one dans les commentaires, il suffit donc de la mise à jour. Le truc c'est qu' includes() attend le nom de l'association, mais l' where attend le nom de la table. Pour un has_one l'association sera généralement exprimé dans le singulier, de sorte que des changements, mais l' where() partie reste tel qu'il est. Donc, si un Person seulement has_one :contact alors votre déclaration serait:

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

Mise à jour 2

Quelqu'un a demandé à propos de l'inverse de la, amis de pas de personnes. Comme je l'ai expliqué ci-après, cela m'a fait réaliser que le dernier champ (ci-dessus: l' :person_id) n'ont pas besoin d'être liés à un modèle que vous êtes de retour, il a juste à être un champ dans la table de jointure. Ils vont tous être nil de sorte qu'il peut être l'un d'eux. Cela conduit à une solution plus simple à ci-dessus:

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

Et puis en passant cette option pour retourner les amis avec aucun peuple devient encore plus simple, vous modifiez uniquement la classe à l'avant:

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

124voto

Unixmonkey Points 7947

C'est encore assez proche de SQL, mais il devrait être à tout le monde avec pas d'amis dans le premier cas:

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

14voto

novemberkilo Points 506

Les 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 faire cela avec Arel par la mise 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)

Les sans amis:

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

12voto

craic.com Points 524

À la fois les réponses de dmarkow et Unixmonkey me procurer ce dont j'ai besoin, je Vous Remercie!

J'ai essayé les deux dans mon application réelle et a obtenu les horaires pour eux - Voici les deux périmètres:

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

Couru ce avec une application réelle - petite table avec ~700 'Personne' enregistrements de la moyenne des 5 courses

Unixmonkey de l'approche (:without_friends_v1) 813ms / requête

dmarkow de l'approche (:without_friends_v2) 891ms / requête (~ 10% plus lent)

Mais alors, il m'est apparu que je n'ai pas besoin de l'appel à DISTINCT()... je suis à la recherche d' Person des dossiers SANS Contacts - donc ils ont juste besoin d'être en NOT IN la liste de contact person_ids. J'ai donc essayé ce champ:

  scope :without_friends_v3, where("id NOT IN (SELECT person_id FROM contacts)")

Qui obtient le même résultat, mais avec une moyenne de 425 ms/appel - près de la moitié du temps...

Maintenant, vous pourriez avoir besoin de l' DISTINCT dans les autres requêtes semblables - mais pour mon cas, cela semble bien fonctionner.

Merci pour votre aide

6voto

Dylan Markow Points 65796

Malheureusement, vous êtes probablement à la recherche à une solution impliquant SQL, mais vous pouvez l'installer dans un champ, puis utilisez simplement que la 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, il vous suffit de faire Person.without_friends, et vous pouvez également la chaîne avec d'autres Arel méthodes: Person.without_friends.order("name").limit(10)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X