Melhor:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Para o hmt, é basicamente a mesma coisa, você confia no fato de que uma pessoa sem amigos também não terá contatos:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Atualizar
Tenho uma pergunta sobre has_onenos comentários, então é só atualizar. O truque aqui é que includes()espera o nome da associação, mas whereespera o nome da tabela. Para um, has_onea associação será geralmente expressa no singular, de modo que muda, mas a where()parte permanece como está. Portanto, se Personapenas has_one :contact, sua declaração seria:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Atualização 2
Alguém perguntou sobre o inverso, amigos sem pessoas. Como eu comentei abaixo, isso realmente me fez perceber que o último campo (acima: the :person_id) não precisa estar relacionado ao modelo que você está retornando, apenas um campo na tabela de junção. Todos eles serão nilpara que possa ser qualquer um deles. Isso leva a uma solução mais simples para o acima:
Person.includes(:contacts).where( :contacts => { :id => nil } )
E, em seguida, alternar para devolver os amigos sem pessoas fica ainda mais simples, você muda apenas a classe na frente:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Atualização 3 - Rails 5
Obrigado a @Anson pela excelente solução Rails 5 (dê alguns + 1s para a resposta abaixo), você pode usar left_outer_joinspara evitar o carregamento da associação:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Eu o incluí aqui para que as pessoas o encontrem, mas ele merece os + 1s por isso. Ótima adição!
Atualização 4 - Rails 6.1
Agradecemos a Tim Park por apontar que, nos próximos 6.1, você poderá fazer o seguinte:
Person.where.missing(:contacts)
Graças ao post que ele vinculou também.