Comment exprimer une requête NOT IN avec ActiveRecord / Rails?

207

Juste pour mettre à jour cela car il semble que beaucoup de gens y viennent, si vous utilisez Rails 4, regardez les réponses de Trung Lê` et VinniVidiVicci.

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

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

J'espère qu'il existe une solution simple qui n'implique pas find_by_sql, sinon je suppose que cela devra fonctionner.

J'ai trouvé cet article qui fait référence à ceci:

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

ce qui est le même que

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

Je me demande s'il y a un moyen de faire NOT INça, comme:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)
Toby Joiner
la source
3
En tant que FYI, Datamapper a eu un support spécifique pour NOT IN. Exemple:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas
1
désolé d'être ignorant, mais qu'est-ce que Datamapper? fait partie des rails 3?
Toby Joiner
2
le mappeur de données est un autre moyen de stocker des données, il remplace Active Record par une structure différente, puis vous écrivez différemment vos éléments liés au modèle tels que les requêtes.
Michael Durrant

Réponses:

313

Rails 4+:

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

Rails 3:

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

actionsest un tableau avec:[1,2,3,4,5]

José Castro
la source
1
C'est la bonne approche avec le dernier modèle de requête Active Record
Nevir
5
@NewAlexandria a raison, vous devez donc faire quelque chose comme Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Cela casserait toujours sur zéro, mais je trouve que le tableau que vous transmettez est généralement généré par un filtre qui retournera []au moins et jamais nul. Je recommande de vérifier Squeel, une DSL au-dessus d'Active Record. Ensuite, vous pouvez faire Topic.where{id.not_in actions}:, nil / vide / ou autre.
danneu
6
@danneu juste échange .empty?pour .blank?et vous êtes nulle épreuve
colllin
(actions.empty?? '', actions) par @daaneu devrait être (actions.empty?? '': actions)
marcel salathe
3
optez pour la notation rails 4: Article.where.not (title: ['Rails 3', 'Rails 5'])
Tal
152

Pour info, dans Rails 4, vous pouvez utiliser la notsyntaxe:

Article.where.not(title: ['Rails 3', 'Rails 5'])
Trung Lê
la source
11
enfin! qu'est-ce qui leur a pris si longtemps pour inclure cela? :)
Dominik Goltermann
50

Vous pouvez essayer quelque chose comme:

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

Vous pourriez avoir besoin de le faire @forums.map(&:id).join(','). Je ne me souviens pas si Rails va l'argument dans une liste CSV si elle est énumérable.

Vous pouvez également faire ceci:

# 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
la source
50

Utilisation d'Arel:

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

ou, si vous préférez:

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

et depuis les rails 4 sur:

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

Veuillez noter que finalement vous ne voulez pas que le forum_ids soit la liste des identifiants, mais plutôt une sous-requête, si c'est le cas, vous devriez faire quelque chose comme ça avant d'obtenir les sujets:

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

de cette façon, vous obtenez tout en une seule requête: quelque chose comme:

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

Notez également que finalement vous ne voulez pas faire cela, mais plutôt une jointure - ce qui pourrait être plus efficace.

Pedro Rolo
la source
2
Une jointure peut être plus efficace, mais pas nécessairement. Assurez-vous d'utiliser EXPLAIN!
James
20

Pour développer la réponse @Trung Lê, dans Rails 4, vous pouvez effectuer les opérations suivantes:

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

Et vous pourriez aller plus loin. Si vous devez d'abord filtrer uniquement les sujets publiés, puis filtrer les identifiants dont vous ne voulez pas, vous pouvez le faire:

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

Rails 4 le rend tellement plus facile!

Vincent Cadoret
la source
12

La solution acceptée échoue si elle @forumsest vide. Pour contourner cela, je devais faire

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

Ou, si vous utilisez Rails 3+:

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

La plupart des réponses ci-dessus devraient vous suffire, mais si vous faites beaucoup plus de ces combinaisons de prédicats et complexes, consultez Squeel . Vous pourrez faire quelque chose comme:

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
la source
2

Vous voudrez peut-être jeter un œil au plugin meta_where d'Ernie Miller. Votre instruction SQL:

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

... pourrait s'exprimer ainsi:

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

Ryan Bates de Railscasts a créé un joli screencast expliquant MetaWhere .

Je ne sais pas si c'est ce que vous recherchez, mais à mes yeux, cela semble certainement mieux qu'une requête SQL intégrée.

Marcin Wyszynski
la source
2

Le message d'origine mentionne spécifiquement l'utilisation d'ID numériques, mais je suis venu ici à la recherche de la syntaxe pour faire un NOT IN avec un tableau de chaînes.

ActiveRecord s'en chargera également pour vous:

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

Ces identifiants de forum peuvent-ils être élaborés de manière pragmatique? par exemple, pouvez-vous trouver ces forums en quelque sorte - si tel est le cas, vous devriez faire quelque chose comme

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

Ce qui serait plus efficace que de faire un SQL not in

Omar Qureshi
la source
1

De cette façon, la lisibilité est optimisée, mais ce n'est pas aussi efficace en termes de requêtes de base de données:

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

Vous pouvez utiliser sql dans vos conditions:

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

Lorsque vous interrogez un tableau vide, ajoutez "<< 0" au tableau dans le bloc where pour qu'il ne renvoie pas "NULL" et interrompez la requête.

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

Si les actions peuvent être un tableau vide ou vide.

itsEconomics
la source
1
Avertissement: cela ajoute en fait un 0 au tableau, il n'est donc plus vide. Il a également pour effet secondaire de modifier le tableau - double danger si vous l'utilisez plus tard. Mieux vaut l'envelopper dans un if-else et utiliser Topic.none / all pour les cas de bord
Ted Pennings
Un moyen plus sûr est:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger
0

Voici une requête "pas dans" plus complexe, utilisant une sous-requête dans les rails 4 à l'aide de Squeel. Bien sûr, très lent par rapport au sql équivalent, mais bon, ça marche.

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

Les 2 premières méthodes de l'étendue sont d'autres étendues qui déclarent les alias cavtl1 et tl1. << est l'opérateur not in de squeel.

J'espère que cela aide quelqu'un.

dukha
la source