Considere uma associação simples ...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
Qual é a maneira mais limpa de obter todas as pessoas que NÃO têm amigos no ARel e / ou meta_where?
E então, que tal um has_many: através da versão
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
Eu realmente não quero usar counter_cache - e pelo que li, não funciona com has_many: através
Eu não quero puxar todos os registros person.friends e percorrê-los no Ruby - quero ter uma consulta / escopo que possa ser usada com a meta meta_search
Não me importo com o custo de desempenho das consultas
E quanto mais longe do SQL real, melhor ...
ruby-on-rails
arel
meta-where
craic.com
fonte
fonte
DISTINCT
. Caso contrário, acho que você deseja normalizar os dados e o índice nesse caso. Eu poderia fazer isso criando umafriend_ids
hstore ou coluna serializada. Então você poderia dizerPerson.where(friend_ids: nil)
not exists (select person_id from friends where person_id = person.id)
(ou talvezpeople.id
oupersons.id
, dependendo da sua tabela). Não tenho certeza do que é mais rápido em uma situação específica, mas no passado isso funcionou bem para mim quando eu não estava tentando usar o ActiveRecord.Melhor:
Para o hmt, é basicamente a mesma coisa, você confia no fato de que uma pessoa sem amigos também não terá contatos:
Atualizar
Tenho uma pergunta sobre
has_one
nos comentários, então é só atualizar. O truque aqui é queincludes()
espera o nome da associação, maswhere
espera o nome da tabela. Para um,has_one
a associação será geralmente expressa no singular, de modo que muda, mas awhere()
parte permanece como está. Portanto, sePerson
apenashas_one :contact
, sua declaração seria: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ãonil
para que possa ser qualquer um deles. Isso leva a uma solução mais simples para o acima:E, em seguida, alternar para devolver os amigos sem pessoas fica ainda mais simples, você muda apenas a classe na frente:
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_joins
para evitar o carregamento da associação: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:
Graças ao post que ele vinculou também.
fonte
has_one
associação, é necessário alterar o nome da associação naincludes
chamada. Assim, supondo que erahas_one :contact
por dentroPerson
, em seguida, seu código seriaPerson.includes(:contact).where( :contacts => { :person_id => nil } )
self.table_name = "custom_friends_table_name"
), usePerson.includes(:friends).where(:custom_friends_table_name => {:id => nil})
.missing
método para fazer exatamente isso !smathy tem uma boa resposta do Rails 3.
Para o Rails 5 , você pode
left_outer_joins
evitar o carregamento da associação.Confira os documentos da API . Foi introduzido na solicitação de recebimento nº 12071 .
fonte
.includes
o custo extra no tempo de carregamento não seria algo que eu me preocuparia muito em otimizar. Seu caso de uso pode ser diferente.Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')
Ele também funciona bem como um escopo. Eu faço isso o tempo todo em meus projetos Rails.includes
, todos esses objetos AR são carregados na memória, o que pode ser uma coisa ruim à medida que as tabelas ficam cada vez maiores. Se você não precisar acessar o registro de contato,left_outer_joins
ele não carregará o contato na memória. A velocidade da solicitação SQL é a mesma, mas o benefício geral do aplicativo é muito maior.Person.where(contacts: nil)
ouPerson.with(contact: contact)
se o uso for muito invasivo da 'propensão' - mas, considerando esse contato: já está sendo analisado e identificado como uma associação, parece lógico que o arel possa facilmente descobrir o que é necessário ...Pessoas que não têm amigos
Ou que tenham pelo menos um amigo
Você pode fazer isso com a Arel configurando escopos em
Friend
E então, pessoas que têm pelo menos um amigo:
Os sem amigos:
fonte
DEPRECATION WARNING: It looks like you are eager loading table(s)
Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
As respostas de dmarkow e Unixmonkey me dão o que eu preciso - obrigado!
Tentei os dois no meu aplicativo real e obtive horários para eles - Aqui estão os dois escopos:
Executei isso com um aplicativo real - mesa pequena com ~ 700 registros 'Pessoa' - média de 5 execuções
Abordagem de Unixmonkey (
:without_friends_v1
) 813ms / consultaabordagem de dmarkow (
:without_friends_v2
) 891ms / consulta (~ 10% mais lenta)Mas então me ocorreu que eu não preciso da ligação para
DISTINCT()...
procurarPerson
registros com NOContacts
- então eles só precisam serNOT IN
a lista de contatosperson_ids
. Então, eu tentei este escopo:O resultado é o mesmo, mas com uma média de 425 ms / chamada - quase metade do tempo ...
Agora você pode precisar de
DISTINCT
outras consultas semelhantes - mas, no meu caso, isso parece funcionar bem.Obrigado pela ajuda
fonte
Infelizmente, você provavelmente está procurando uma solução envolvendo SQL, mas pode defini-la em um escopo e depois usá-lo:
Em seguida, para obtê-los, você pode simplesmente fazer isso
Person.without_friends
e também pode encadear isso com outros métodos da Arel:Person.without_friends.order("name").limit(10)
fonte
Uma subconsulta correlacionada NOT EXISTS deve ser rápida, principalmente à medida que a contagem de linhas e a proporção de registros filho / pai aumentam.
fonte
Além disso, para filtrar por um amigo, por exemplo:
fonte