Mejor:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Por el momento, es básicamente lo mismo, confías en el hecho de que una persona sin amigos tampoco tendrá contactos:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Actualizar
Tengo una pregunta sobre has_oneen los comentarios, así que solo actualizando. El truco aquí es que includes()espera el nombre de la asociación pero whereespera el nombre de la tabla. Para a, has_onela asociación generalmente se expresará en singular, de modo que cambia, pero la where()parte permanece como está. Entonces, si Personsolo una has_one :contact, su declaración sería:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Actualización 2
Alguien preguntó por el inverso, amigos sin gente. Como comenté a continuación, esto realmente me hizo darme cuenta de que el último campo (arriba: el :person_id) en realidad no tiene que estar relacionado con el modelo que está devolviendo, solo tiene que ser un campo en la tabla de unión. Todos van a ser nilasí que puede ser cualquiera de ellos. Esto lleva a una solución más simple a lo anterior:
Person.includes(:contacts).where( :contacts => { :id => nil } )
Y luego cambiar esto para devolver a los amigos sin gente se vuelve aún más simple, solo cambias la clase en el frente:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Actualización 3 - Rails 5
Gracias a @Anson por la excelente solución Rails 5 (darle algunos +1 por su respuesta a continuación), puede usar left_outer_joinspara evitar cargar la asociación:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Lo he incluido aquí para que la gente lo encuentre, pero se merece los +1 por esto. Gran adición!
Actualización 4 - Rails 6.1
Gracias a Tim Park por señalar que en el próximo 6.1 puedes hacer esto:
Person.where.missing(:contacts)
Gracias a la publicación a la que también se vinculó.