ActiveRecord.find (array_of_ids), menjaga ketertiban

98

Saat Anda melakukannya Something.find(array_of_ids)di Rails, urutan larik yang dihasilkan tidak bergantung pada urutan array_of_ids.

Apakah ada cara untuk menemukan dan melestarikan pesanan?

ATM Saya mengurutkan catatan secara manual berdasarkan urutan ID, tetapi itu agak timpang.

UPD: jika mungkin untuk menentukan urutan menggunakan :orderparam dan beberapa jenis klausa SQL, lalu bagaimana?

Leonid Shevtsov
sumber

Jawaban:

71

Jawabannya hanya untuk mysql

Ada fungsi di mysql yang disebut FIELD ()

Berikut adalah bagaimana Anda dapat menggunakannya di .find ():

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

Pembaruan: Ini akan dihapus dalam kode sumber Rails 6.1

kovyrin.dll
sumber
Apakah Anda kebetulan mengetahui padanan FIELDS()di Postgres?
Trung Lê
3
Saya menulis fungsi plpgsql untuk melakukan ini di postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
Ini tidak lagi berfungsi. Untuk Rails terbaru:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
Ini adalah solusi yang lebih baik daripada Gunchars karena tidak akan merusak pagination.
pguardiario
.ids berfungsi dengan baik untuk saya, dan merupakan dokumentasi
aktiver yang
79

Anehnya, tidak ada yang menyarankan sesuatu seperti ini:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

Efisien karena selain membiarkan backend SQL melakukannya.

Sunting: Untuk meningkatkan jawaban saya sendiri, Anda juga dapat melakukannya seperti ini:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_bydan #slicemerupakan tambahan yang sangat berguna di ActiveSupport untuk array dan hashes.

Gunchars
sumber
Jadi pengeditan Anda tampaknya berfungsi tetapi itu membuat saya gugup urutan kunci dalam hash tidak dijamin, bukan? jadi ketika Anda memanggil slice dan mendapatkan hash kembali "diurutkan ulang" itu benar-benar bergantung pada nilai yang dikembalikan hash dalam urutan kuncinya ditambahkan. Ini terasa seperti bergantung pada detail implementasi yang mungkin berubah.
Jon
2
@Jon, urutan dijamin di Ruby 1.9 dan setiap implementasi lain yang mencoba mengikutinya. Untuk 1.8, Rails (ActiveSupport) menambal kelas Hash untuk membuatnya berperilaku sama, jadi jika Anda menggunakan Rails, Anda akan baik-baik saja.
Gunchars
terima kasih atas klarifikasinya, baru saja ditemukan di dokumentasi.
Jon
13
Masalahnya adalah ia mengembalikan array, bukan relasi.
Velizar Hristov
3
Hebat, bagaimanapun, satu baris tidak bekerja untuk saya (Rails 4.1)
Besi
44

Seperti yang dinyatakan Mike Woodhouse dalam jawabannya , hal ini terjadi karena, di bawah tenda, Rails menggunakan kueri SQL dengan aWHERE id IN... clause untuk mengambil semua rekaman dalam satu kueri. Ini lebih cepat daripada mengambil setiap id satu per satu, tetapi seperti yang Anda perhatikan, ini tidak menjaga urutan catatan yang Anda ambil.

Untuk memperbaikinya, Anda dapat mengurutkan catatan di tingkat aplikasi sesuai dengan daftar asli ID yang Anda gunakan saat mencari catatan.

Berdasarkan banyak jawaban bagus untuk Mengurutkan array sesuai dengan elemen array lain , saya merekomendasikan solusi berikut:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

Atau jika Anda membutuhkan sesuatu yang sedikit lebih cepat (tetapi bisa dibilang agak kurang dapat dibaca) Anda dapat melakukan ini:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi 32
sumber
3
solusi kedua (dengan index_by) tampaknya gagal untuk saya, menghasilkan semua hasil nihil.
Ben Wheeler
22

Ini tampaknya berfungsi untuk postgresql ( sumber ) - dan mengembalikan relasi ActiveRecord

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(order)
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

dapat diperpanjang untuk kolom lain juga, misalnya untuk namekolom, gunakan position(name::text in ?)...

jahe
sumber
Anda adalah pahlawan saya minggu ini. Terima kasih!
ntdb
4
Perhatikan bahwa ini hanya berfungsi dalam kasus-kasus sepele, Anda akhirnya akan menghadapi situasi di mana Id Anda terkandung dalam ID lain dalam daftar (misalnya akan menemukan 1 dari 11). Salah satu cara untuk menyiasatinya adalah dengan menambahkan koma ke dalam pemeriksaan posisi, dan kemudian menambahkan koma terakhir ke penggabungan, seperti ini: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Poin bagus, @IrishDubGuy! Saya akan memperbarui jawaban saya berdasarkan saran Anda. Terima kasih!
jahe
bagi saya rantai tidak berhasil. Di sini nama tabel harus ditambahkan sebelum id: teks seperti ini: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] versi lengkap yang berfungsi untuk saya: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } terima kasih @gingerlime @IrishDubGuy
pengguna1136228
Saya rasa Anda perlu menambahkan nama tabel jika Anda melakukan beberapa penggabungan ... Itu cukup umum dengan cakupan ActiveRecord saat Anda bergabung.
jahe
19

Seperti yang saya jawab di sini , saya baru saja merilis permata ( order_as_specified ) yang memungkinkan Anda melakukan pengurutan SQL asli seperti ini:

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

Sejauh yang saya bisa uji, ini berfungsi secara native di semua RDBMS, dan mengembalikan relasi ActiveRecord yang bisa dirantai.

JacobEvelyn
sumber
1
Bung, kamu sangat luar biasa. Terima kasih!
swrobel
5

Sayangnya, tidak mungkin dalam SQL yang akan berfungsi di semua kasus, Anda harus menulis temuan tunggal untuk setiap catatan atau urutan dalam ruby, meskipun mungkin ada cara untuk membuatnya berfungsi menggunakan teknik berpemilik:

Contoh pertama:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

SANGAT CANGGIH

Contoh kedua:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
sumber
Meskipun tidak terlalu efisien, saya setuju penyelesaian ini adalah DB-agnostik dan dapat diterima jika Anda memiliki jumlah baris yang kecil.
Trung Lê
Jangan gunakan suntik untuk ini, ini adalah peta:sorted = arr.map { |val| Model.find(val) }
tokland
yang pertama lambat. Saya setuju dengan yang kedua dengan peta seperti ini:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon
2

Jawaban @Gunchars bagus, tetapi tidak berhasil di luar kotak di Rails 2.3 karena kelas Hash tidak dipesan. Solusi sederhana adalah dengan memperluas kelas Enumerable ' index_byuntuk menggunakan kelas OrderedHash:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

Sekarang pendekatan @Gunchars akan berhasil

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

Bonus

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

Kemudian

Something.find_with_relevance(array_of_ids)
Chris Bloom
sumber
2

Dengan asumsi Model.pluck(:id)kembali [1,2,3,4]dan Anda menginginkan urutan[2,4,1,3]

Konsepnya adalah dengan memanfaatkan ORDER BY CASE WHENklausa SQL. Sebagai contoh:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

Di Rails, Anda dapat mencapai ini dengan memiliki metode publik dalam model Anda untuk membangun struktur yang serupa:

def self.order_by_ids(ids)
  if ids.present?
    order_by = ["CASE"]
    ids.each_with_index do |id, index|
      order_by << "WHEN id='#{id}' THEN #{index}"
    end
    order_by << "END"
    order(order_by.join(" "))
  end
else
  all # If no ids, just return all
end

Kemudian lakukan:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

Hal yang baik tentang ini. Hasil dikembalikan sebagai ActiveRecord Relations (yang memungkinkan Anda untuk menggunakan metode seperti last, count, where, pluck, dll)

Christian Fazzini
sumber
2

Ada permata find_with_order yang memungkinkan Anda melakukannya secara efisien dengan menggunakan kueri SQL asli.

Dan itu mendukung Mysqldan PostgreSQL.

Sebagai contoh:

Something.find_with_order(array_of_ids)

Jika Anda menginginkan hubungan:

Something.where_with_order(:id, array_of_ids)
khiav reoy
sumber
1

Di bawah kap, finddengan serangkaian id akan menghasilkan klausa SELECTwith WHERE id IN..., yang seharusnya lebih efisien daripada perulangan melalui id.

Jadi permintaan terpenuhi dalam satu perjalanan ke database, tetapi SELECTtanpa ORDER BYklausa tidak diurutkan. ActiveRecord memahami ini, jadi kami memperluas findsebagai berikut:

Something.find(array_of_ids, :order => 'id')

Jika urutan id dalam array Anda sewenang-wenang dan signifikan (yaitu Anda ingin urutan baris dikembalikan agar sesuai dengan array Anda terlepas dari urutan id yang terkandung di dalamnya) maka saya pikir Anda akan menjadi server terbaik dengan pasca-pemrosesan hasilnya di kode - Anda dapat membuat :orderklausa tetapi itu akan menjadi sangat rumit dan sama sekali tidak mengungkapkan niat.

Mike Woodhouse
sumber
Perhatikan bahwa hash opsi sudah tidak digunakan lagi. (argumen kedua, dalam contoh ini :order => id)
ocodo
1

Meskipun saya tidak melihatnya disebutkan di mana pun di CHANGELOG, sepertinya fungsi ini telah diubah dengan rilis versi 5.2.0.

Di sini komit memperbarui dokumen yang ditandai dengan 5.2.0Namun tampaknya juga telah di- backport ke versi 5.0.

Pembunuh XML
sumber
0

Dengan mengacu pada jawabannya di sini

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") bekerja untuk Postgresql.

Sam Kah Chiin
sumber
-4

Ada klausa order di find (: order => '...') yang melakukan ini saat mengambil record. Anda juga bisa mendapatkan bantuan dari sini.

teks tautan

Ashish Jain
sumber