Sie möchten in Rails 3 Datensätze ohne verknüpfte Datensätze finden

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, die keine Freunde in ARel und / oder Meta_where haben?

Und was ist mit einer Version von has_many: through

 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: through

Ich möchte nicht alle person.friends-Datensätze abrufen und diese in Ruby durchlaufen – ich möchte eine Abfrage / einen Bereich haben, den ich mit dem meta_search-Juwel verwenden kann

Die Kosten für die performance der Abfragen sind mir egal

Und je weiter weg von SQL, desto besser …

   

    Dies ist immer noch ziemlich nah an SQL, aber es sollte im ersten Fall alle ohne Freunde kommen:

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

    Besser:

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

    Für den hmt ist es im Grunde dasselbe, man verlässt sich darauf, dass eine Person ohne Freunde auch keine Kontakte hat:

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

    Aktualisieren

    Haben Sie eine Frage zu has_one in den Kommentaren, also aktualisieren Sie einfach. Der Trick dabei ist, dass includes() den Namen der Assoziation erwartet, aber die where den Namen der Tabelle erwartet. Für eine has_one die Assoziation im Allgemeinen in der Einzahl ausgedrückt, so dass sich Änderungen ergeben, aber der where() Teil bleibt so wie er ist. Wenn also eine Person nur has_one :contact hat, wäre Ihre Aussage:

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

    Update 2

    Jemand fragte nach dem Gegenteil, Freunde ohne Leute. Wie ich unten :person_id habe, wurde mir klar, dass das letzte Feld (oben: die :person_id ) nicht unbedingt mit dem Modell, das Sie zurückgeben, in Beziehung stehen muss, es muss nur ein Feld in der Join-Tabelle sein. Sie werden alle nil also kann es jeder von ihnen sein. Dies führt zu einer einfacheren Lösung für das oben genannte:

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

    Und dann wird das Umschalten auf die Rückkehr der Freunde ohne Leute noch einfacher, du änderst nur die class an der Front:

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

    Update 3 – Rails 5

    Dank @Anson für die ausgezeichnete Rails 5 Lösung (geben Sie ihm ein paar +1 für seine Antwort unten), können Sie 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, aber er verdient die + 1s dafür. Tolle Ergänzung!

    Smathy hat eine gute Antwort von Rails 3.

    Für Rails 5 können Sie left_outer_joins , um das Laden der Assoziation zu vermeiden.

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

    Schau dir die API-Dokumente an . Es wurde in Pull-Anforderung # 12071 eingeführt .

    Personen, die keine Freunde haben

     Person.includes(:friends).where("friends.person_id IS NULL") 

    Oder die mindestens einen Freund haben

     Person.includes(:friends).where("friends.person_id IS NOT NULL") 

    Sie können dies mit Arel tun, indem Sie Bereiche auf Friend einrichten

     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 Freundlose:

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

    Beide Antworten von dmarkow und Unixmonkey bekommen mich, was ich brauche – Danke!

    Ich habe beides in meiner App ausprobiert und habe Timings für sie – Hier sind die zwei 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 

    Lief das mit einer echten App – kleine Tabelle mit ~ 700 ‘Person’ Aufzeichnungen – Durchschnitt von 5 Läufen

    Unixmonkey Ansatz ( :without_friends_v1 ) 813ms / Abfrage

    dmarkow’s Ansatz ( :without_friends_v2 ) 891ms / query (~ 10% langsamer)

    Aber dann fiel mir ein, dass ich den Aufruf von DISTINCT()... nicht brauche DISTINCT()... Ich suche nach Person Contacts – sie müssen also NOT IN der Liste der Kontaktperson_IDs person_ids . Also habe ich diesen Bereich ausprobiert:

      scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") } 

    Das ergibt dasselbe Ergebnis, aber mit durchschnittlich 425 ms / Anruf – fast die Hälfte der Zeit …

    Jetzt brauchen Sie vielleicht die DISTINCT in anderen ähnlichen Abfragen – aber für meinen Fall scheint das gut zu funktionieren.

    Danke für Ihre Hilfe

    Unglücklicherweise suchen Sie wahrscheinlich nach einer Lösung mit SQL, aber Sie können sie in einem Bereich definieren und dann nur diesen Bereich 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 

    Um sie zu bekommen, kannst du einfach Person.without_friends , und du kannst dies auch mit anderen Arel-Methoden Person.without_friends.order("name").limit(10) : Person.without_friends.order("name").limit(10)

    Eine NOT EXISTS-korrelierte Unterabfrage sollte schnell sein, insbesondere wenn die Zeilenanzahl und das Verhältnis von Kind- zu Elterndatensätzen zunimmt.

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

    Um beispielsweise von einem Freund herausgefiltert zu werden:

     Friend.where.not(id: other_friend.friends.pluck(:id))