Como expressar uma consulta NOT IN com ActiveRecord / Rails?

207

Apenas para atualizar isso, pois parece que muitas pessoas chegam a isso, se você estiver usando o Rails 4, observe as respostas de Trung Lê` e VinniVidiVicci.

Topic.where.not(forum_id:@forums.map(&:id))

Topic.where(published:true).where.not(forum_id:@forums.map(&:id))

Espero que exista uma solução fácil que não envolva find_by_sql, se não, acho que terá que funcionar.

Encontrei este artigo que faz referência a isso:

Topic.find(:all, :conditions => { :forum_id => @forums.map(&:id) })

que é o mesmo que

SELECT * FROM topics WHERE forum_id IN (<@forum ids>)

Gostaria de saber se existe uma maneira de fazer NOT INisso, como:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)
Toby Joiner
fonte
3
Como FYI, o Datamapper teve suporte específico para NOT IN. Exemplo:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas
1
desculpe por ser ignorante, mas o que é o Datamapper? isso faz parte dos trilhos 3?
Toby Joiner
2
O mapeador de dados é uma maneira alternativa de armazenar dados, substitui o Active Record por uma estrutura diferente e, em seguida, você escreve o material relacionado ao modelo, como consultas, de maneira diferente.
Michael Durrant

Respostas:

313

Trilhos 4+:

Article.where.not(title: ['Rails 3', 'Rails 5']) 

Trilhos 3:

Topic.where('id NOT IN (?)', Array.wrap(actions))

Onde actionsestá uma matriz com:[1,2,3,4,5]

José Castro
fonte
1
Esta é a abordagem adequada com o mais recente modelo de consulta Active Record
Nevir
5
@NewAlexandria está certo, então você teria que fazer algo assim Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Ele ainda quebraria em zero, mas acho que a matriz que você passa geralmente é gerada por um filtro que retornará []no mínimo e nunca nulo. Eu recomendo verificar o Squeel, um DSL no topo do Active Record. Então você pode fazer Topic.where{id.not_in actions}:, nil / empty / ou de outra forma.
danneu
6
@danneu apenas troca .empty?de .blank?e você é nil-prova
colllin
(actions.empty '', acções por @daaneu deve ser (actions.empty???) ':' ações)
marcel Salathe
3
ir para os carris 4 a notação: Article.where.not (título: [ 'Os carris 3', 'guias 5'])
Tal
152

FYI, no Rails 4, você pode usar a notsintaxe:

Article.where.not(title: ['Rails 3', 'Rails 5'])
Trung Lê
fonte
11
finalmente! por que demoraram tanto para incluir isso? :)
Dominik Goltermann 4/13
50

Você pode tentar algo como:

Topic.find(:all, :conditions => ['forum_id not in (?)', @forums.map(&:id)])

Você pode precisar fazer @forums.map(&:id).join(','). Não me lembro se o Rails incluirá o argumento em uma lista CSV, se for enumerável.

Você também pode fazer isso:

# in topic.rb
named_scope :not_in_forums, lambda { |forums| { :conditions => ['forum_id not in (?)', forums.select(&:id).join(',')] }

# in your controller 
Topic.not_in_forums(@forums)
Jonnii
fonte
50

Usando o Arel:

topics=Topic.arel_table
Topic.where(topics[:forum_id].not_in(@forum_ids))

ou, se preferir:

topics=Topic.arel_table
Topic.where(topics[:forum_id].in(@forum_ids).not)

e desde os trilhos 4 em:

topics=Topic.arel_table
Topic.where.not(topics[:forum_id].in(@forum_ids))

Observe que, eventualmente, você não deseja que os forum_ids sejam a lista de IDs, mas uma subconsulta; nesse caso, você deve fazer algo assim antes de obter os tópicos:

@forum_ids = Forum.where(/*whatever conditions are desirable*/).select(:id)

dessa maneira, você obtém tudo em uma única consulta: algo como:

select * from topic 
where forum_id in (select id 
                   from forum 
                   where /*whatever conditions are desirable*/)

Observe também que, eventualmente, você não deseja fazer isso, mas uma junção - o que pode ser mais eficiente.

Pedro Rolo
fonte
2
Uma junção pode ser mais eficiente, mas não necessariamente. Certifique-se de usar EXPLAIN!
James
20

Para expandir a resposta @Trung Lê, no Rails 4 você pode fazer o seguinte:

Topic.where.not(forum_id:@forums.map(&:id))

E você poderia dar um passo adiante. Se você precisar primeiro filtrar apenas os Tópicos publicados e depois filtrar os IDs que não deseja, faça o seguinte:

Topic.where(published:true).where.not(forum_id:@forums.map(&:id))

O Rails 4 facilita muito!

Vincent Cadoret
fonte
12

A solução aceita falhará se @forumsestiver vazia. Para contornar isso, eu tive que fazer

Topic.find(:all, :conditions => ['forum_id not in (?)', (@forums.empty? ? '' : @forums.map(&:id))])

Ou, se estiver usando o Rails 3+:

Topic.where( 'forum_id not in (?)', (@forums.empty? ? '' : @forums.map(&:id)) ).all
Filipe Giusti
fonte
4

A maioria das respostas acima deve bastar para você, mas se você estiver fazendo muito mais combinações de predicado e complexo, consulte Squeel . Você será capaz de fazer algo como:

Topic.where{{forum_id.not_in => @forums.map(&:id)}}
Topic.where{forum_id.not_in @forums.map(&:id)} 
Topic.where{forum_id << @forums.map(&:id)}
Jake
fonte
2

Você pode dar uma olhada no plugin meta_where de Ernie Miller. Sua instrução SQL:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)

... poderia ser expresso assim:

Topic.where(:forum_id.nin => @forum_ids)

Ryan Bates, do Railscasts, criou um belo screencast explicando o MetaWhere .

Não tenho certeza se é isso que você está procurando, mas aos meus olhos certamente parece melhor do que uma consulta SQL incorporada.

Marcin Wyszynski
fonte
2

O post original menciona especificamente o uso de IDs numéricos, mas eu vim aqui procurando a sintaxe para fazer um NOT IN com uma matriz de strings.

O ActiveRecord também cuidará disso muito bem:

Thing.where(['state NOT IN (?)', %w{state1 state2}])
Andy Triggs
fonte
1

Esses IDs de fórum podem ser elaborados de maneira pragmática? por exemplo, você pode encontrar esses fóruns de alguma forma - se for esse o caso, faça algo como

Topic.all(:joins => "left join forums on (forums.id = topics.forum_id and some_condition)", :conditions => "forums.id is null")

O que seria mais eficiente do que fazer um SQL not in

Omar Qureshi
fonte
1

Dessa maneira, otimiza a legibilidade, mas não é tão eficiente em termos de consultas ao banco de dados:

# Retrieve all topics, then use array subtraction to
# find the ones not in our list
Topic.all - @forums.map(&:id)
evanrmurphy
fonte
0

Você pode usar o sql nas suas condições:

Topic.find(:all, :conditions => [ "forum_id NOT IN (?)", @forums.map(&:id)])
tjeden
fonte
0

Quando você consulta uma matriz em branco, adicione "<< 0" à matriz no bloco where para que não retorne "NULL" e quebre a consulta.

Topic.where('id not in (?)',actions << 0)

Se as ações pudessem ser uma matriz vazia ou em branco.

itsEconomics
fonte
1
Aviso: na verdade, ele adiciona um 0 ao array, para que não fique mais vazio. Ele também tem o efeito colateral de modificar a matriz - risco duplo, se você usá-la mais tarde. Muito melhor para envolvê-la em um if-else e usar Topic.none / all para os casos de ponta
Ted Pennings
Uma maneira mais segura é:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger
0

Aqui está uma consulta "não in" mais complexa, usando uma subconsulta no Rails 4 usando squeel. Claro que muito lento comparado ao sql equivalente, mas ei, funciona.

    scope :translations_not_in_english, ->(calmapp_version_id, language_iso_code){
      join_to_cavs_tls_arr(calmapp_version_id).
      joins_to_tl_arr.
      where{ tl1.iso_code == 'en' }.
      where{ cavtl1.calmapp_version_id == my{calmapp_version_id}}.
      where{ dot_key_code << (Translation.
        join_to_cavs_tls_arr(calmapp_version_id).
        joins_to_tl_arr.    
        where{ tl1.iso_code == my{language_iso_code} }.
        select{ "dot_key_code" }.all)}
    }

Os 2 primeiros métodos no escopo são outros escopos que declaram os aliases cavtl1 e tl1. << é o operador não no squeel.

Espero que isso ajude alguém.

dukha
fonte