Sie möchten Aufzeichnungen ohne zugehörige Aufzeichnungen in Rails finden möchten
-
24-10-2019 - |
Frage
Betrachten Sie eine einfache Assoziation ...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
Was ist der sauberste Weg, um alle Personen zu bekommen, die keine Freunde in Arel und/oder Meta_where haben?
Und was ist dann mit einem Has_Many: durch 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
Ich möchte wirklich nicht counter_cache verwenden - und ich von dem, was ich gelesen habe, funktioniert es nicht mit Has_Many: durch
Ich möchte nicht alle Person mit Freunden ziehen und in Ruby durch sie durchgehen - ich möchte eine Abfrage/einen Umfang haben, den ich mit dem Edelstein für meta_search verwenden kann
Es macht mir nichts aus, die Leistungskosten der Fragen
Und je weiter von der tatsächlichen SQL entfernt ist, desto besser ...
Lösung
Dies ist SQL immer noch ziemlich nahe, aber es sollte alle ohne Freunde im ersten Fall bringen:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Andere Tipps
Besser:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Für die HMT ist es im Grunde dasselbe, Sie verlassen sich darauf, dass eine Person ohne Freunde auch keine Kontakte hat:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Aktualisieren
Habe eine Frage zu has_one
In den Kommentaren, also nur aktualisiert. Der Trick hier ist das includes()
erwartet den Namen des Vereins, aber der where
erwartet den Namen der Tabelle. Für ein has_one
Die Vereinigung wird im Allgemeinen im Singular ausgedrückt, so dass sich verändert, aber der where()
Teil bleibt wie es ist. Also wenn a Person
nur has_one :contact
Dann wäre Ihre Aussage:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Update 2
Jemand fragte nach dem Inversen, Freunde ohne Menschen. Wie ich unten kommentierte, merkte mich das tatsächlich, dass das letzte Feld (oben: das: das :person_id
) muss eigentlich nicht mit dem Modell verwandt sein, das Sie zurückkehren, es muss nur ein Feld in der Join -Tabelle sein. Sie werden alle sein nil
So kann es jeder von ihnen sein. Dies führt zu einer einfacheren Lösung für die oben genannte:
Person.includes(:contacts).where( :contacts => { :id => nil } )
Und dann wechseln Sie dies, um die Freunde ohne Menschen zurückzugeben, noch einfacher. Sie ändern nur die Klasse an der Vorderseite:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Update 3 - Rails 5
Vielen Dank an @anson für die exzellente Rails 5 -Lösung (gib ihm einige +1 für seine Antwort unten), können Sie verwenden left_outer_joins
Um das Laden der Assoziation zu vermeiden:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Ich habe es hier aufgenommen, damit die Leute es finden werden, aber er verdient die +1 dafür. Tolle Ergänzung!
Smathy hat eine gute Antwort 3 Antwort.
Für Schienen 5, Sie können verwenden left_outer_joins
Um die Assoziation zu beladen.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Probier das aus API -Dokumente. Es wurde in Pull Anfrage eingeführt #12071.
Personen, die keine Freunde haben
Person.includes(:friends).where("friends.person_id IS NULL")
Oder das hat mindestens einen Freund
Person.includes(:friends).where("friends.person_id IS NOT NULL")
Sie können dies mit Arel tun, indem Sie Bereiche einrichten 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
Und dann Personen, die mindestens einen Freund haben:
Person.includes(:friends).merge(Friend.to_somebody)
Der Freundlosen:
Person.includes(:friends).merge(Friend.to_nobody)
Sowohl die Antworten von Dmarkow als auch Unixmonkey bekommen mir was ich brauche - danke!
Ich habe beide in meiner echten App ausprobiert und habe Zeit für sie - hier sind die beiden Bereiche:
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
Führte dies mit einer echten App - kleiner Tisch mit ~ 700 'Person' Records - Durchschnitt von 5 Läufen
Unixmonkeys Ansatz (:without_friends_v1
) 813 ms / Abfrage
Dmarkows Ansatz (:without_friends_v2
) 891 ms / Abfrage (~ 10% langsamer)
Aber dann fiel mir ein, dass ich den Anruf nicht brauche DISTINCT()...
Ich suche Person
Aufzeichnungen mit Nr Contacts
- Also müssen sie es einfach sein NOT IN
die Liste des Kontakts person_ids
. Also habe ich diesen Bereich ausprobiert:
scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
Das bekommt das gleiche Ergebnis, aber mit durchschnittlich 425 ms/Anruf - fast die Hälfte der Zeit ...
Jetzt brauchst du vielleicht die DISTINCT
In anderen ähnlichen Fragen scheint dies gut zu funktionieren.
Danke für Ihre Hilfe
Leider betrachten Sie wahrscheinlich eine Lösung mit SQL, aber Sie könnten sie in einen Bereich einstellen und dann nur diesen Umfang verwenden:
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
Dann, um sie zu bekommen, können Sie einfach tun Person.without_friends
, und Sie können dies auch mit anderen Arel -Methoden ketten: Person.without_friends.order("name").limit(10)
Eine nicht existierende korrelierte Unterabfrage sollte schnell sein, insbesondere wenn die Anzahl der Zeilen und das Verhältnis von Kind zu übergeordneten Aufzeichnungen zunimmt.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Auch zum Beispiel von einem Freund herausfiltern:
Friend.where.not(id: other_friend.friends.pluck(:id))