Як висловити запит 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
Як FYI, Datamapper мав спеціальну підтримку для NOT IN. Приклад:Person.all(:name.not => ['bob','rick','steve'])
Марк Томас

1
Вибачте за те, що не знаєте, але що таке Datamapper? це частина рейок 3?
Toby Joiner

2
Картограф даних - це альтернативний спосіб зберігання даних, він замінює 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}nil / empty / або іншим чином.
danneu

6
@danneu просто заміна .empty?для .blank?і ви нуль доказ
colllin

(Actions.empty?? '', Actions) від @daaneu should be (Actions.empty?? '':
Actions

3
піти на рейки 4 позначення: Article.where.not (назва: ['Рейки 3', 'Рейки 5'])
Тал

152

FYI, в 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)

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)

0

Ви можете використовувати sql у своїх умовах:

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


0

Коли ви запитуєте порожній масив, додайте "<< 0" до масиву в блоці, де він не повертається "NULL" і розбиває запит.

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

Якщо дії можуть бути порожнім або порожнім масивом.


1
Попередження: це фактично додає 0 до масиву, тому він більше не порожній. Він також має побічний ефект від зміни масиву - подвійну небезпеку, якщо використовувати його пізніше. Набагато краще загорнути його в інше і використовувати Topic.none / все для крайових справ
Тед Пеннінг

Більш безпечним способом є:Topic.where("id NOT IN (?)", actions.presence || [0])
Вестон Гангер

0

Ось більш складний запит "не в", використовуючи підзапит у рейках 4, використовуючи віск. Звичайно дуже повільно в порівнянні з еквівалентним 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)}
    }

Перші два способи в області застосування - це інші області, в яких оголошуються псевдоніми cavtl1 і tl1. << - оператор, який не ввімкнеться.

Сподіваюся, що це комусь допоможе.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.