Bagaimana cara mengekspresikan permintaan NOT IN dengan ActiveRecord / Rails?

207

Hanya untuk memperbarui ini karena tampaknya banyak orang datang ke ini, jika Anda menggunakan Rails 4 lihat jawaban oleh Trung Lê` dan VinniVidiVicci.

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

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

Saya berharap ada solusi mudah yang tidak melibatkan find_by_sql, jika tidak maka saya rasa itu harus berhasil.

Saya menemukan artikel ini yang mereferensikan ini:

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

yang sama dengan

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

Saya bertanya-tanya apakah ada cara untuk melakukan NOT INitu, seperti:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)
Toby Joiner
sumber
3
Sebagai FYI, Datamapper telah memiliki dukungan khusus untuk NOT IN. Contoh:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas
1
maaf karena tidak tahu apa-apa, tapi apa itu Datamapper? Apakah itu bagian dari rel 3?
Toby Joiner
2
data mapper adalah cara alternatif untuk menyimpan data, itu menggantikan Rekaman Aktif dengan struktur yang berbeda dan kemudian Anda menulis hal-hal terkait model Anda seperti pertanyaan, berbeda.
Michael Durrant

Jawaban:

313

Rails 4+:

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

Rel 3:

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

Di mana actionsarray dengan:[1,2,3,4,5]

José Castro
sumber
1
Ini adalah pendekatan yang tepat dengan model kueri Rekaman Aktif terbaru
Nevir
5
@NewAlexandria benar, jadi Anda harus melakukan sesuatu seperti Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Itu masih akan rusak pada nil, tapi saya menemukan bahwa array yang Anda lewati biasanya dihasilkan oleh filter yang akan kembali []paling tidak dan tidak pernah nihil. Saya sarankan memeriksa Squeel, sebuah DSL di atas Rekaman Aktif. Maka Anda bisa melakukan Topic.where{id.not_in actions}:, nihil / kosong / atau sebaliknya.
danneu
6
@danneu hanya swap .empty?untuk .blank?dan Anda nol-bukti
colllin
(action.empty?? '', aksi) oleh @daaneu harus (action.empty?? '': aksi)
marcel salathe
3
pergi untuk notasi rails 4: Article.where.not (judul: ['Rails 3', 'Rails 5'])
Tal
152

FYI, Di Rails 4, Anda dapat menggunakan notsintaks:

Article.where.not(title: ['Rails 3', 'Rails 5'])
Trung Lê
sumber
11
akhirnya! apa yang membuat mereka begitu lama memasukkannya? :)
Dominik Goltermann
50

Anda dapat mencoba sesuatu seperti:

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

Anda mungkin perlu melakukannya @forums.map(&:id).join(','). Saya tidak ingat apakah Rails akan argumen ke daftar CSV jika itu enumerable.

Anda juga bisa melakukan ini:

# 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
sumber
50

Menggunakan Arel:

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

atau, jika disukai:

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

dan karena rel 4 pada:

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

Harap perhatikan bahwa pada akhirnya Anda tidak ingin forum_ids menjadi daftar id, melainkan subquery, jika demikian maka Anda harus melakukan sesuatu seperti ini sebelum mendapatkan topik:

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

dengan cara ini Anda mendapatkan semuanya dalam satu permintaan: sesuatu seperti:

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

Perhatikan juga bahwa pada akhirnya Anda tidak ingin melakukan ini, melainkan bergabung - apa yang mungkin lebih efisien.

Pedro Rolo
sumber
2
Bergabung mungkin lebih efisien, tetapi tidak harus. Pastikan untuk menggunakan EXPLAIN!
James
20

Untuk memperluas jawaban @ Trung Lê, di Rails 4 Anda dapat melakukan hal berikut:

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

Dan Anda bisa melangkah lebih jauh. Jika Anda perlu memfilter pertama hanya untuk Topik yang dipublikasikan dan kemudian menyaring id yang tidak Anda inginkan, Anda bisa melakukan ini:

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

Rails 4 membuatnya jauh lebih mudah!

Vincent Cadoret
sumber
12

Solusi yang diterima gagal jika @forumskosong. Untuk mengatasinya, saya harus melakukannya

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

Atau, jika menggunakan Rails 3+:

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

Sebagian besar jawaban di atas sudah cukup bagi Anda, tetapi jika Anda melakukan lebih banyak kombinasi predikat dan kompleks seperti itu periksa Squeel . Anda akan dapat melakukan sesuatu seperti:

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
sumber
2

Anda mungkin ingin melihat plugin meta_where oleh Ernie Miller. Pernyataan SQL Anda:

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

... dapat diekspresikan seperti ini:

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

Ryan Bates dari Railscasts menciptakan screencast yang bagus untuk menjelaskan MetaWhere .

Tidak yakin apakah ini yang Anda cari tetapi bagi saya, ini jelas terlihat lebih baik daripada kueri SQL yang disematkan.

Marcin Wyszynski
sumber
2

Posting asli secara khusus menyebutkan menggunakan ID numerik, tapi saya datang ke sini mencari sintaks untuk melakukan NOT IN dengan array string.

ActiveRecord akan menanganinya dengan baik untuk Anda juga:

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

Bisakah id forum ini dikerjakan secara pragmatis? misalnya Anda dapat menemukan forum ini entah bagaimana - jika itu masalahnya Anda harus melakukan sesuatu seperti

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

Yang mana akan lebih efisien daripada melakukan SQL not in

Omar Qureshi
sumber
1

Cara ini mengoptimalkan keterbacaan, tetapi tidak seefisien dalam hal permintaan basis data:

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

Anda dapat menggunakan sql dalam kondisi Anda:

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

Ketika Anda query array kosong tambahkan "<< 0" ke array di blok where sehingga tidak mengembalikan "NULL" dan pecahkan query.

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

Jika tindakan bisa berupa array kosong atau kosong.

Ekonomi
sumber
1
Peringatan: ini sebenarnya menambahkan 0 pada array, jadi tidak lagi kosong. Ini juga memiliki efek samping dari memodifikasi array - bahaya ganda jika Anda menggunakannya nanti. Jauh lebih baik untuk membungkusnya dalam if-else dan menggunakan Topic.none / all untuk kasus tepi
Ted Pennings
Cara yang lebih aman adalah:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger
0

Berikut adalah query "tidak dalam" yang lebih kompleks, menggunakan subquery di rails 4 menggunakan squeel. Tentu saja sangat lambat dibandingkan dengan sql yang setara, tapi hei, itu berhasil.

    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 metode pertama dalam ruang lingkup adalah ruang lingkup lain yang menyatakan alias cavtl1 dan tl1. << adalah operator yang tidak ada dalam squeel.

Semoga ini bisa membantu seseorang.

dukha
sumber