Lepszy:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Hmt to w zasadzie to samo, polegasz na tym, że osoba bez przyjaciół również nie będzie miała kontaktów:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Aktualizacja
Mam pytanie has_onew komentarzach, więc po prostu aktualizuję. Sztuczka polega na tym, że includes()oczekuje nazwy skojarzenia, ale whereoczekuje nazwy tabeli. W przypadku a has_oneskojarzenie będzie zazwyczaj wyrażane w liczbie pojedynczej, więc zmienia się, ale where()część pozostaje taka, jaka jest. Więc jeśli Persontylko has_one :contactwtedy twoje oświadczenie byłoby:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Zaktualizuj 2
Ktoś zapytał o odwrotność, przyjaciół bez ludzi. Jak skomentowałem poniżej, to faktycznie uświadomiło mi, że ostatnie pole (powyżej: the :person_id) tak naprawdę nie musi być związane z modelem, który zwracasz, po prostu musi to być pole w tabeli łączenia. Wszyscy będą, nilwięc może to być każdy z nich. Prowadzi to do prostszego rozwiązania powyższego:
Person.includes(:contacts).where( :contacts => { :id => nil } )
A potem przełączenie tego, aby zwrócić przyjaciół bez ludzi, staje się jeszcze prostsze, zmieniasz tylko klasę z przodu:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Aktualizacja 3 - Rails 5
Dzięki @Anson za doskonałe rozwiązanie Rails 5 (daj mu kilka + 1-ek za jego odpowiedź poniżej), możesz użyć, left_outer_joinsaby uniknąć ładowania skojarzenia:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Umieściłem go tutaj, aby ludzie go mogli znaleźć, ale zasługuje na +1 za to. Świetny dodatek!
Aktualizacja 4 - Rails 6.1.0
Podziękowania dla Tima Park za wskazanie, że w nadchodzącym 6.1 możesz to zrobić:
Person.where.missing(:contacts)
Dzięki postowi, do którego też się podlinkował