Question

Soit une simple association ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

Quelle est la plus propre façon d'obtenir toutes les personnes qui ne disposent pas des amis dans Arel et / ou meta_where?

Et alors qu'en une 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 je ce que je l'ai lu ne fonctionne pas avec has_many: par

Je ne veux pas tirer tous les enregistrements de person.friends et boucle à travers eux dans Ruby - Je veux avoir une requête / portée que je peux utiliser avec la gemme meta_search

Je ne me dérange pas le coût de la performance des requêtes

Et plus loin de SQL réelle mieux ...

Était-ce utile?

La solution

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

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

Autres conseils

Mieux:

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

Pour la HMT, il est fondamentalement la même chose, vous compter sur le fait qu'une personne sans amis aura pas non plus de contacts:

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

Mise à jour

Vous avez une question sur has_one dans les commentaires, donc juste la mise à jour. L'astuce ici est que includes() attend le nom de l'association, mais le where attend le nom de la table. Pour une has_one l'association sera exprimée généralement au singulier, de sorte que les changements, mais la partie where() reste comme il est. Donc, si un Person ne has_one :contact alors votre déclaration serait:

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

Mise à jour 2

Quelqu'un a demandé sur l'inverse, les amis sans les gens. Comme je l'ai commenté ci-dessous, cela m'a vraiment fait réaliser que le dernier champ (ci-dessus: le :person_id) n'a pas réellement être lié au modèle que vous êtes de retour, il doit juste être un champ dans la table de jointure. Ils vont tous être nil il peut donc être l'un d'eux. Cela conduit à une solution plus simple à ce qui précède:

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

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

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

Mise à jour 3 - Rails 5

Merci à @Anson pour l'excellent Rails 5 solution (lui donner quelques + 1s 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 afin que les gens trouveront, mais il mérite le + 1s pour cela. Excellent ajout!

smathy a une bonne Rails 3 réponse.

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

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

Consultez la docs api. Il a été introduit dans la demande de traction # 12071 .

Les personnes qui ne disposent pas des 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 amis:

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

Les deux réponses dmarkow et Unixmonkey me obtenir ce que je dois - Thank You!

J'ai essayé à la fois dans mon application réelle et timings obtenu 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

Ran cela avec une application réelle - petite table avec ~ 700 dossiers 'personne' - moyenne de 5 pistes

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

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

Mais il me est apparu que je ne ai pas besoin de l'appel à DISTINCT()... Je suis à la recherche des dossiers de Person avec NO Contacts - donc ils doivent être NOT IN la liste des contacts 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 du DISTINCT dans d'autres requêtes similaires -. Mais pour mon cas, ce qui semble fonctionner correctement

Merci pour votre aide

Malheureusement, vous êtes probablement à une solution impliquant SQL, mais vous pouvez définir dans un champ, puis il suffit d'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 également enchaîner cela avec d'autres méthodes Arel: Person.without_friends.order("name").limit(10)

A NOT EXISTS sous-requête corrélée doit être rapide, d'autant plus que le nombre de lignes et le rapport de l'enfant à fiches parent augmente.

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

En outre, pour filtrer par un ami par exemple:

Friend.where.not(id: other_friend.friends.pluck(:id))
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top