Vous voulez trouver des enregistrements sans enregistrements associés dans Rails
-
24-10-2019 - |
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 ...
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!
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))