Como expressar uma consulta NOT IN com o ActiveRecord / Rails?

Apenas para atualizar isso, já que parece que muitas pessoas chegam a isso, se você estiver usando o Rails 4, veja 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)) 

Eu estou esperando que haja uma solução fácil que não envolva find_by_sql , se não, então eu acho que terá que funcionar.

Eu 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 () 

Eu estou querendo saber se há uma maneira de fazer isso com NOT IN , como:

 SELECT * FROM topics WHERE forum_id NOT IN () 

    Eu estou usando isso:

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

    Onde actions é uma matriz com: [1,2,3,4,5]

    Editar:

    Para notação do Rails 4:

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

    FYI, No Rails 4, você pode usar not syntax:

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

    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 irá inserir o argumento em uma lista CSV, se ela 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) 

    Usando 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 rails 4 em:

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

    Por favor note que, eventualmente, você não quer que o forum_ids seja a lista de ids, mas sim uma subconsulta, então você deve fazer algo assim antes de obter os tópicos:

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

    Desta forma, 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 quer fazer isso, mas sim uma junit – o que pode ser mais eficiente.

    Para expandir a resposta do @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 filtrar primeiro apenas os tópicos publicados e filtrar os IDs que não deseja, faça o seguinte:

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

    Rails 4 torna muito mais fácil!

    A solução aceita falhará se @forums estiver vazio. 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 

    A maioria das respostas acima deve ser suficiente, mas se você estiver fazendo muito mais dessas combinações de predicados e complexas, verifique 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)} 

    Você pode querer dar uma olhada no plugin meta_where de Ernie Miller. Sua declaraçã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 agradável 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.

    Esses ids de fórum podem ser trabalhados de forma pragmática? Por exemplo, você pode encontrar estes fóruns de alguma forma – se esse for o caso, você deve fazer algo como

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

    Qual seria mais eficiente do que fazer um SQL not in

    Desta forma, otimiza a legibilidade, mas não é tão eficiente em termos de consultas ao database:

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

    A postagem original menciona especificamente o uso de IDs numéricos, mas eu vim aqui procurando a syntax para fazer NOT IN com uma matriz de strings.

    O ActiveRecord lidará bem com isso para você também:

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

    Você pode usar o SQL em suas condições:

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

    Piggybacking de jonnii:

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

    usando arrancar ao invés de mapear os elementos

    encontrado via railsconf 2012 10 coisas que você não sabia rails poderia fazer

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

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

    Se as ações puderem ser um array vazio ou em branco.

    Aqui está uma consulta “não em” mais complexa, usando uma subconsulta em rails 4 usando squeel. Claro muito lento em comparação com o sql equivalente, mas hey, 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 dois primeiros methods no escopo são outros escopos que declaram os aliases cavtl1 e tl1. < < é o operador que não está no squeel.

    Espero que isso ajude alguém.