Лучше:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Для hmt это в основном то же самое, вы полагаетесь на то, что у человека без друзей также не будет контактов:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Обновить
Есть вопрос о has_oneв комментариях, так что просто обновление. Хитрость в том, чтоincludes() ожидает имя ассоциации, но whereожидает имя таблицы. Для has_oneассоциации обычно будет выражаться в единственном числе, так что меняется, но where()часть остается такой, как есть. Так что, если Personтолько has_one :contactтогда ваше заявление будет:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Обновление 2
Кто-то спрашивал об обратном, друзья без людей. Как я прокомментировал ниже, это фактически заставило меня осознать, что последнее поле (выше :person_id:) не обязательно должно быть связано с возвращаемой моделью, оно просто должно быть полем в таблице соединений. Они все будут, nilтак что это может быть любой из них. Это приводит к более простому решению вышеперечисленного:
Person.includes(:contacts).where( :contacts => { :id => nil } )
И затем переключение на возвращение друзей без людей становится еще проще, вы меняете только класс впереди:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Обновление 3 - Rails 5
Спасибо @Anson за отличное решение для Rails 5 (дайте ему +1 к ответу ниже), вы можете использовать left_outer_joins чтобы избежать загрузки ассоциации:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Я включил это здесь, чтобы люди нашли это, но он заслуживает +1 для этого. Отличное дополнение!
Обновление 4 - Rails 6.1
Спасибо Tim Park за указание, что в следующей версии 6.1 вы можете сделать это:
Person.where.missing(:contacts)
Благодаря посту, на который он тоже ссылался.