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

207

Просто чтобы обновить это, так как кажется, что многие приходят к этому, если вы используете Rails 4, посмотрите на ответы Trung Lê` и VinniVidiVicci.

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

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

Я надеюсь, что есть простое решение, которое не включает 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>)
Тоби Столяр
источник
3
Как к сведению, Datamapper имеет особую поддержку NOT IN. Пример:Person.all(:name.not => ['bob','rick','steve'])
Марк Томас
1
извините за невежество, но что такое Datamapper? это часть рельсов 3?
Тоби Столяр
2
Data Mapper - это альтернативный способ хранения данных, он заменяет Active Record другой структурой, а затем вы по-другому пишете связанные с моделью вещи, такие как запросы.
Майкл Даррант

Ответы:

313

Рельсы 4+:

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

Рельсы 3:

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

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

Хосе Кастро
источник
1
Это правильный подход с последней моделью запросов Active Record
Nevir
5
@NewAlexandria права, так что вам придется сделать что-то вроде Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Он все равно будет равен нулю, но я обнаружил, что передаваемый массив обычно генерируется фильтром, который будет возвращать []по крайней мере и никогда не будет равен нулю. Я рекомендую проверить Squeel, DSL поверх Active Record. Тогда вы можете сделать:, Topic.where{id.not_in actions}ноль / пусто / или иначе.
Даннеу
6
@danneu просто замена .empty?для .blank?и вы ноль доказательство
colllin
(actions.empty?? '', actions) @daaneu должен быть (actions.empty?? '': actions)
марсель салат
3
перейти к обозначению рельсов 4: Article.where.not (название: ['Rails 3', 'Rails 5'])
Tal
152

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

Article.where.not(title: ['Rails 3', 'Rails 5'])
Трунг Лэ
источник
11
Ну наконец то! что заставило их так долго включать это? :)
Доминик Гольтерманн
50

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

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)
jonnii
источник
50

Используя 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*/)

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

Педро Роло
источник
2
Объединение может быть более эффективным, но не обязательно. Обязательно используйте EXPLAIN!
Джеймс
20

Чтобы расширить ответ @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 делает это намного проще!

Винсент Кадорет
источник
12

Принятое решение не выполняется, если @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
Филипе Джусти
источник
4

Вам должно хватить большинства приведенных выше ответов, но если вы делаете гораздо больше таких предикатов и сложных комбинаций, проверьте 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)}
Джейк
источник
2

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

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

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

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

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

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

Марчин Вышински
источник
2

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

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

Thing.where(['state NOT IN (?)', %w{state1 state2}])
Энди Триггс
источник
1

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

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

Что было бы более эффективно, чем делать SQL not in

Омар Куреши
источник
1

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

# Retrieve all topics, then use array subtraction to
# find the ones not in our list
Topic.all - @forums.map(&:id)
evanrmurphy
источник
0

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

Topic.find(:all, :conditions => [ "forum_id NOT IN (?)", @forums.map(&:id)])
tjeden
источник
0

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

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

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

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

Вот более сложный запрос «не в», использующий подзапрос в рельсах 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. << не является оператором в squeel.

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

дукха
источник