Come esprimere una query NOT IN con ActiveRecord / Rails?


207

Solo per aggiornare questo dato che sembra che molte persone arrivino a questo, se stai usando Rails 4 guarda le risposte di Trung Lê` e VinniVidiVicci.

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

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

Spero che ci sia una soluzione semplice che non implichi find_by_sql, altrimenti credo che dovrà funzionare.

Ho trovato questo articolo che fa riferimento a questo:

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

che è lo stesso di

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

Mi chiedo se c'è un modo per farlo NOT IN, come:

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

3
In quanto FYI, Datamapper ha avuto un supporto specifico per NOT IN. Esempio:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas,

1
scusa per l'ignoranza, ma cos'è Datamapper? fa parte di Rails 3?
Toby Joiner,

2
il mappatore di dati è un modo alternativo di archiviare i dati, sostituisce il Record attivo con una struttura diversa e quindi scrivi cose relative al modello come le query, in modo diverso.
Michael Durrant,

Risposte:


313

Rotaie 4+:

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

Rotaie 3:

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

Dov'è actionsun array con:[1,2,3,4,5]


1
Questo è l'approccio corretto con l'ultimo modello di query Record attivo
Nevir

5
@NewAlexandria ha ragione, quindi dovresti fare qualcosa del genere Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Si interromperebbe comunque con zero, ma trovo che l'array in cui si passa sia generalmente generato da un filtro che tornerà []per lo meno e mai zero. Consiglio di dare un'occhiata a Squeel, un DSL in cima ad Active Record. Quindi potresti fare Topic.where{id.not_in actions}:, zero / vuoto / o altrimenti.
danneu

6
@danneu solo di swap .empty?per .blank?e siete nil-proof
colllin

(actions.empty?? '', actions) di @daaneu dovrebbe essere (actions.empty?? '': actions)
marcel salathe

3
scegli la notazione rails 4: Article.where.not (titolo: ['Rails 3', 'Rails 5'])
Tal

152

Cordiali saluti, In Rails 4, è possibile utilizzare la notsintassi:

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

11
finalmente! cosa ci è voluto così tanto tempo per includerlo? :)
Dominik Goltermann il

50

Puoi provare qualcosa del tipo:

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

Potrebbe essere necessario farlo @forums.map(&:id).join(','). Non ricordo se Rails includerà l'argomento in un elenco CSV se è enumerabile.

Puoi anche fare questo:

# 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)

50

Utilizzando Arel:

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

o, se preferito:

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

e poiché le rotaie 4 su:

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

Si noti che alla fine non si desidera che forum_ids sia la lista degli ID, ma piuttosto una sottoquery, in tal caso è necessario fare qualcosa del genere prima di ottenere gli argomenti:

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

in questo modo ottieni tutto in una singola query: qualcosa del tipo:

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

Nota anche che alla fine non vuoi farlo, ma piuttosto un join: cosa potrebbe essere più efficiente.


2
Un join potrebbe essere più efficiente, ma non necessariamente. Assicurati di usare EXPLAIN!
James,

20

Per espandere la risposta @Trung Lê, in Rails 4 puoi effettuare le seguenti operazioni:

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

E potresti fare un ulteriore passo avanti. Se devi prima filtrare solo gli argomenti pubblicati e poi filtrare gli ID che non desideri, puoi farlo:

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

Rails 4 lo rende molto più semplice!


12

La soluzione accettata ha esito negativo se @forumsè vuota. Per ovviare a questo ho dovuto fare

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

Oppure, se si utilizza Rails 3+:

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

4

La maggior parte delle risposte di cui sopra dovrebbe bastarti, ma se stai facendo molte più di queste combinazioni predicate e complesse, controlla Squeel . Sarai in grado di fare qualcosa del tipo:

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)}

2

Potresti dare un'occhiata al plugin meta_where di Ernie Miller. La tua istruzione SQL:

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

... potrebbe essere espresso in questo modo:

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

Ryan Bates of Railscasts ha creato un bel screencast che spiega MetaWhere .

Non sono sicuro se questo è quello che stai cercando, ma ai miei occhi sembra sicuramente migliore di una query SQL incorporata.


2

Il post originale menziona specificamente l'uso di ID numerici, ma sono venuto qui cercando la sintassi per fare un NOT IN con una matrice di stringhe.

ActiveRecord lo gestirà bene anche per te:

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

1

Questi ID forum possono essere elaborati in modo pragmatico? ad esempio puoi trovare questi forum in qualche modo - se è così dovresti fare qualcosa del genere

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

Quale sarebbe più efficiente che fare un SQL not in


1

In questo modo ottimizza per la leggibilità, ma non è così efficiente in termini di query del database:

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

0

Puoi usare sql nelle tue condizioni:

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


0

Quando si esegue una query su un array vuoto, aggiungere "<< 0" all'array nel blocco Where in modo che non restituisca "NULL" e interrompere la query.

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

Se le azioni potrebbero essere un array vuoto o vuoto.


1
Avvertenza: questo aggiunge effettivamente uno 0 all'array, quindi non è più vuoto. Ha anche l'effetto collaterale di modificare l'array: doppio pericolo se lo usi in seguito. Molto meglio avvolgerlo in un if-else e usare Topic.none / all per i casi limite
Ted Pennings

Un modo più sicuro è:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger,

0

Ecco una query "non in" più complessa, usando una sottoquery in rails 4 usando squeel. Certo molto lento rispetto all'equivalente sql, ma ehi, funziona.

    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)}
    }

I primi 2 metodi nell'ambito sono altri ambiti che dichiarano gli alias cavtl1 e tl1. << è l'operatore non in squeel.

Spero che questo aiuti qualcuno.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.