Как выразить запрос NOT IN с помощью ActiveRecord / Rails?

Я надеюсь, что есть простое решение, которое не требует find_by_sql, если нет, то, думаю, это сработает.

Я нашел эту статью, в которой упоминается это :

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

который совпадает с

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

Мне интересно, есть ли способ сделать NOT IN с этим, например:

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

person Toby Joiner    schedule 29.11.2010    source источник
comment
К вашему сведению, Datamapper имеет особую поддержку NOT IN. Пример: Person.all(:name.not => ['bob','rick','steve'])   -  person Mark Thomas    schedule 29.11.2010
comment
извините за невежество, но что такое Datamapper? это часть рельсов 3?   -  person Toby Joiner    schedule 30.11.2010
comment
Data Mapper - это альтернативный способ хранения данных, он заменяет Active Record другой структурой, а затем вы по-другому пишете вещи, связанные с вашей моделью, такие как запросы.   -  person Michael Durrant    schedule 13.07.2011


Ответы (16)


Рельсы 4+:

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

Рельсы 3:

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

Где actions - это массив с: [1,2,3,4,5]

person José Castro    schedule 25.07.2011
comment
Это правильный подход с последней моделью запросов Active Record. - person Nevir; 09.03.2012
comment
@NewAlexandria права, поэтому вам нужно сделать что-то вроде Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Он все равно прервется на nil, но я обнаружил, что массив, который вы передаете, обычно генерируется фильтром, который, по крайней мере, возвращает [] и никогда не возвращает nil. Я рекомендую попробовать Squeel, DSL поверх Active Record. Тогда вы можете сделать: Topic.where{id.not_in actions}, nil / empty / или иначе. - person danneu; 18.01.2013
comment
@danneu просто замените .empty? на .blank?, и вы ничего не добьетесь - person colllin; 05.02.2013
comment
(actions.empty?? '', actions) от @daaneu должно быть (actions.empty?? '': actions) - person marcel salathe; 05.02.2014
comment
Я думаю, что функция, возвращающая массив действий, должна заботиться о том, чтобы он был [] вместо nil - person Yo Ludke; 20.05.2014
comment
перейти к обозначению рельсов 4: Article.where.not (title: ['Rails 3', 'Rails 5']) - person Tal; 15.02.2015
comment
Как сказала @NewAlexandria, это не работает, если переменная равна [] или nil. У вас получится id NOT in (null). Чтобы обойти это, используйте presence, например: Topic.where( 'id NOT IN (?)', actions.presence || "" ) - person Joshua Pinter; 30.03.2018
comment
@JoshuaPinter просто используйте Array.wrap(actions) - person toxaq; 29.05.2018

К вашему сведению, в Rails 4 вы можете использовать синтаксис not:

Article.where.not(title: ['Rails 3', 'Rails 5'])
person Trung Lê    schedule 16.08.2013
comment
наконец-то! почему они так долго включали это? :) - person Dominik Goltermann; 04.10.2013

Вы можете попробовать что-то вроде:

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

Возможно, вам придется сделать @forums.map(&:id).join(','). Я не могу вспомнить, внесет ли Rails аргумент в список CSV, если он перечислим.

Вы также можете сделать это:

# 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)
person jonnii    schedule 29.11.2010

Использование Arel:

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

или, если желательно:

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

а так как рельсы 4 на:

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

Обратите внимание, что в конечном итоге вы хотите, чтобы forum_ids не был списком идентификаторов, а скорее подзапросом, в таком случае вам следует сделать что-то вроде этого, прежде чем получать темы:

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

таким образом вы получите все в одном запросе: что-то вроде:

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

Также обратите внимание, что в конечном итоге вы захотите не это делать, а скорее объединение - что может быть более эффективным.

person Pedro Rolo    schedule 14.04.2011
comment
Соединение может быть более эффективным, но не обязательно. Обязательно используйте EXPLAIN! - person James; 21.01.2014

Чтобы расширить ответ @Trung Lê, в Rails 4 вы можете сделать следующее:

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

И вы могли бы сделать еще один шаг. Если вам нужно сначала отфильтровать только опубликованные темы, а затем затем отфильтровать идентификаторы, которые вам не нужны, вы можете сделать следующее:

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

Rails 4 делает это намного проще!

person Vincent Cadoret    schedule 22.01.2014

Принятое решение не выполняется, если @forums пусто. Чтобы обойти это, мне пришлось сделать

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

Или, если вы используете Rails 3+:

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

Вам должно хватить большинства приведенных выше ответов, но если вы делаете гораздо больше таких предикатов и сложных комбинаций, проверьте Squeel . Вы сможете делать что-то вроде:

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)}
person jake    schedule 14.09.2011

Возможно, вам стоит взглянуть на плагин meta_where Эрни Миллера. Ваш оператор SQL:

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

... можно выразить так:

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

Райан Бейтс из Railscasts создал хороший скринкаст, объясняющий MetaWhere.

Не уверен, что это то, что вы ищете, но, на мой взгляд, это определенно выглядит лучше, чем встроенный SQL-запрос.

person Marcin Wyszynski    schedule 18.04.2011

В исходном сообщении специально упоминается использование числовых идентификаторов, но я пришел сюда в поисках синтаксиса для выполнения NOT IN с массивом строк.

ActiveRecord прекрасно справится с этим и для вас:

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

Можно ли разработать идентификаторы форумов с практической точки зрения? например можете ли вы как-нибудь найти эти форумы - в таком случае вам следует сделать что-нибудь вроде

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

Это было бы эффективнее, чем выполнение SQL not in

person Omar Qureshi    schedule 29.11.2010

Этот способ оптимизирует читаемость, но не так эффективен с точки зрения запросов к базе данных:

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

Вы можете использовать sql в своих условиях:

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

Копирование jonnii:

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

использование выщипывания, а не отображение элементов

найдено с помощью railsconf 2012 10 вещей, которые вы не знал, что рельсы можно сделать

person Thomas Wolfe    schedule 15.10.2012

Когда вы запрашиваете пустой массив, добавьте «‹< 0» к массиву в блоке where, чтобы он не возвращал «NULL» и не прерывал запрос.

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

Если действия могут быть пустым или пустым массивом.

person itsEconomics    schedule 29.11.2013
comment
Предупреждение: это фактически добавляет к массиву 0, поэтому он больше не пустой. У него также есть побочный эффект изменения массива - двойная опасность, если вы воспользуетесь им позже. Намного лучше обернуть его в if-else и использовать Topic.none / all для крайних случаев. - person Ted Pennings; 03.12.2013
comment
Более безопасный способ: Topic.where("id NOT IN (?)", actions.presence || [0]) - person Weston Ganger; 18.05.2018

Вот более сложный запрос "не в", использующий подзапрос в rails 4 с использованием squeel. Конечно, очень медленно по сравнению с эквивалентным sql, но это работает.

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

Первые 2 метода в области видимости - это другие области, которые объявляют псевдонимы cavtl1 и tl1. ‹< - оператор not in в squeel.

Надеюсь, это кому-то поможет.

person dukha    schedule 10.08.2015

Если кто-то хочет использовать два или более условий, вы можете сделать это:

your_array = [1,2,3,4]
your_string = "SOMETHING"

YourModel.where('variable1 NOT IN (?) AND variable2=(?)',Array.wrap(your_array),your_string)

person LEONARDO PEREIRA RODRIGUES    schedule 11.11.2020