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 ...
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)')
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 } )
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 } )
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 } )
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!
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)
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
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)
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)")
En outre, pour filtrer par un ami par exemple:
Friend.where.not(id: other_friend.friends.pluck(:id))