¿Cómo expresar una consulta NOT IN con ActiveRecord / Rails?

207

Solo para actualizar esto, ya que parece que mucha gente viene a esto, si está utilizando Rails 4, vea las respuestas de Trung Lê` y VinniVidiVicci.

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

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

Espero que haya una solución fácil que no implique find_by_sql, si no, supongo que tendrá que funcionar.

Encontré este artículo que hace referencia a esto:

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

que es lo mismo que

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

Me pregunto si hay una manera de hacer NOT INeso, como:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)
Toby Joiner
fuente
3
Como FYI, Datamapper ha tenido soporte específico para NOT IN. Ejemplo:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas el
1
perdón por ser ignorante, pero ¿qué es Datamapper? ¿Es esa parte de los rieles 3?
Toby Joiner
2
El mapeador de datos es una forma alternativa de almacenar datos, reemplaza Active Record con una estructura diferente y luego escribe cosas relacionadas con su modelo, como consultas, de manera diferente.
Michael Durrant

Respuestas:

313

Rieles 4+:

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

Carriles 3:

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

¿Dónde actionsestá una matriz con:[1,2,3,4,5]

José Castro
fuente
1
Este es el enfoque adecuado con el último modelo de consulta de Active Record
Nevir el
55
@NewAlexandria tiene razón, así que tendrías que hacer algo como eso Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Todavía se rompería en cero, pero creo que la matriz que pasa generalmente es generada por un filtro que regresará []al menos y nunca será cero. Recomiendo echar un vistazo a Squeel, un DSL además de Active Record. Entonces podría hacer: Topic.where{id.not_in actions}nulo / vacío / o de otra manera.
danneu
66
@danneu sólo cambio .empty?para .blank?y que son nulas a prueba
colllin
(actions.empty?? '', actions) por @daaneu debería ser (actions.empty?? '': actions)
marcel salathe
3
vaya a la notación de rails 4: Article.where.not (título: ['Rails 3', 'Rails 5'])
Tal
152

Para su información, en Rails 4, puede usar la notsintaxis:

Article.where.not(title: ['Rails 3', 'Rails 5'])
Trung Lê
fuente
11
¡finalmente! ¿Qué les llevó tanto tiempo incluir eso? :)
Dominik Goltermann
50

Puedes probar algo como:

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

Es posible que deba hacer @forums.map(&:id).join(',') . No recuerdo si Rails incluirá el argumento en una lista CSV si es enumerable.

También puedes hacer esto:

# 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
fuente
50

Usando Arel:

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

o, si se prefiere:

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

y como los carriles 4 en:

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

Tenga en cuenta que eventualmente no desea que forum_ids sea la lista de identificadores, sino una subconsulta, si es así, debe hacer algo como esto antes de obtener los temas:

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

de esta manera obtienes todo en una sola consulta: algo así como:

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

También tenga en cuenta que eventualmente no desea hacer esto, sino una unión, lo que podría ser más eficiente.

Pedro Rolo
fuente
2
Una unión puede ser más eficiente, pero no necesariamente. ¡Asegúrate de usar EXPLAIN!
James
20

Para ampliar la respuesta @Trung Lê, en Rails 4 puede hacer lo siguiente:

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

Y podrías ir un paso más allá. Si primero necesita filtrar solo los Temas publicados y luego filtrar los identificadores que no desea, puede hacer esto:

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

¡Rails 4 lo hace mucho más fácil!

Vincent Cadoret
fuente
12

La solución aceptada falla si @forumsestá vacía. Para solucionar esto tuve que hacer

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

O, si usa Rails 3+:

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

La mayoría de las respuestas anteriores deberían ser suficientes, pero si está haciendo muchas más de tales combinaciones predicadas y complejas, consulte Squeel . Podrás hacer 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
fuente
2

Es posible que desee echar un vistazo al complemento meta_where de Ernie Miller. Su declaración SQL:

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

... podría expresarse así:

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

Ryan Bates de Railscasts creó un bonito screencast explicando MetaWhere .

No estoy seguro de si esto es lo que está buscando, pero a mi parecer, ciertamente se ve mejor que una consulta SQL incrustada.

Marcin Wyszynski
fuente
2

La publicación original menciona específicamente el uso de ID numéricos, pero vine aquí buscando la sintaxis para hacer un NOT IN con una serie de cadenas.

ActiveRecord también lo manejará muy bien para usted:

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

¿Se pueden resolver estos identificadores de foro de forma pragmática? por ejemplo, ¿puedes encontrar estos foros de alguna manera? Si ese es el caso, deberías hacer algo como

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

Lo cual sería más eficiente que hacer un SQL not in

Omar Qureshi
fuente
1

De esta manera, se optimiza la legibilidad, pero no es tan eficiente en términos de consultas a la base de datos:

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

Puede usar sql en sus condiciones:

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

Cuando consulta una matriz en blanco, agregue "<< 0" a la matriz en el bloque where para que no devuelva "NULL" y rompa la consulta.

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

Si las acciones podrían ser una matriz vacía o en blanco.

itsEconomics
fuente
1
Advertencia: esto realmente agrega un 0 a la matriz, por lo que ya no está vacío. También tiene el efecto secundario de modificar la matriz: doble peligro si la usa más tarde. Mucho mejor para envolverlo en un if-else y usar Topic.none / all para los casos extremos
Ted Pennings
Una forma más segura es:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger
0

Aquí hay una consulta más compleja "no en", usando una subconsulta en rails 4 usando squeel. Por supuesto, muy lento en comparación con el sql equivalente, pero bueno, 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)}
    }

Los primeros 2 métodos en el alcance son otros ámbitos que declaran los alias cavtl1 y tl1. << es el operador no en chirrido.

Espero que esto ayude a alguien.

dukha
fuente