Soru ActiveRecord nesnelerini belirtilen sırayla kimliğine göre bulmanın temiz yolu


Bir dizi kimlik verilen bir ActiveRecord nesnesi dizisi almak istiyorum.

Bunu kabul ettim

Object.find([5,2,3])

Nesne 5, nesne 2, sonra bu sırada nesne 3 ile bir dizi döndürür, ancak bunun yerine nesne 2, nesne 3 ve sonra nesne 5 olarak sipariş edilen bir dizi olsun.

ActiveRecord Tabanı yöntem API'sini bul verilen sırada beklememeniz gerektiğinden bahseder (diğer belgeler bu uyarıyı vermez).

Bir potansiyel çözüm verildi Aynı sırayla ids dizisi ile bul?Ancak, sipariş seçeneği SQLite için geçerli görünmüyor.

Nesneleri kendim sıralamak için biraz ruby ​​kodu yazabilirim (biraz basit ve yetersiz ölçekleme veya daha iyi ölçekleme ve daha karmaşık), ama A Better Way var mı?


25
2018-04-29 10:53


Menşei


Bu kimlik nereden geliyor? Kullanıcı arayüzüyse (onları seçen bir kullanıcı aracılığıyla), ölçeklendirme bir sorun olmamalıdır; bu, kullanıcının 1000 kimliğini seçerken zaman harcaması muhtemel değildir. Veri tabanı (ör. Bir birleşim tablosundan) ise, siparişi birleştirme tablosunda saklayabilir ve bulmacayı buna göre düzenleyebilir misiniz? - pauliephonic
Bu gibi görünüyor artık doğru değil Rails 5'te. - XML Slayer


Cevaplar:


MySQL ve diğer DB'lerin işleri kendi başlarına sıralaması değil, onları sıralamaması. Aradığın zaman Model.find([5, 2, 3])oluşturulan SQL şöyle bir şeydir:

SELECT * FROM models WHERE models.id IN (5, 2, 3)

Bu bir sipariş belirtmiyor, sadece geri dönmek istediğiniz kayıtların kümesi. Genelde MySQL veritabanı satırlarını geri döndürecektir. 'id' sipariş, ama bunun garantisi yok.

Veritabanını garantili bir sırayla kayıtlara döndürmek için tek yol bir sipariş cümlesi eklemektir. Kayıtlarınız her zaman belirli bir sırayla gönderilecekse, db'ye bir sıralama sütunu ekleyebilir ve Model.find([5, 2, 3], :order => 'sort_column'). Durum böyle değilse, sıralamayı kod içinde yapmanız gerekir:

ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}} 

22
2018-04-30 07:41



O (N) performansından kaçınmak için #detect kullanmaktan daha performanslı bir başka çözüm: records = Model.find (ids) .group_by (&: id); sıralanmış_records = ids.map {| id | kayıtlar [id] .first} - Ryan LeCompte


Jeroen van Dijk'e yaptığım önceki yorumuma dayanarak, bunu daha verimli ve iki satır kullanarak yapabilirsiniz. each_with_object

result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}

Burada referans olarak kullandığım ölçüt

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)

Benchmark.measure do 
  100000.times do 
    result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
    ids.map {|id| result_hash[id]}
  end
end.real
#=>  4.45757484436035 seconds

Şimdi diğeri

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do 
  100000.times do 
    ids.collect {|id| results.detect {|result| result.id == id}}
  end
end.real
# => 6.10875988006592

Güncelleştirme 

Bunu en çok sipariş ve vaka deyimlerini kullanarak yapabilirsiniz, burada kullanabileceğiniz bir sınıf yöntemidir.

def self.order_by_ids(ids)
  order_by = ["case"]
  ids.each_with_index.map do |id, index|
    order_by << "WHEN id='#{id}' THEN #{index}"
  end
  order_by << "end"
  order(order_by.join(" "))
end

#   User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#   #=> [3,2,1]

10
2017-09-13 19:19





Görünüşe göre, mySQL ve diğer DB yönetim sistemi işleri kendi başlarına sıralıyor. Bunu yapmanın yanından geçebileceğini düşünüyorum:

ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )

6
2018-04-29 16:13



Bu, "Aynı sırayla id dizileri ile bul" bağlantısında önerilen cevaptı, ancak SQLite için işe yaramıyor. - Andrew Grimm
Ve Postgres 9 için çalışmıyor gibi görünüyor - Jeroen van Dijk
Bu artık çalışmıyor. Daha yeni Rails için: Object.where(id: ids).order("field(id, #{ids.join ','})") - mahemoff
Gelişmiş: Object.where(id: ids).order(ids.present? && "field(id, #{ids.join ','})"). Bu, kimlik dizisi boş veya sıfır ise bir SQL hatası engeller. - mahemoff


Taşınabilir bir çözüm, ORDER BY'nizde bir SQL CASE ifadesi kullanmak olacaktır. SİPARİŞ BY'de herhangi bir ifadeyi kullanabilirsiniz ve bir CASE, çizgili bir arama tablosu olarak kullanılabilir. Örneğin, peşinde olduğunuz SQL şöyle görünecektir:

select ...
order by
    case id
    when 5 then 0
    when 2 then 1
    when 3 then 2
    end

Biraz Ruby ile üretmek oldukça kolay:

ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'

Yukarıdakiler, sayılarla veya başka güvenli değerler ile çalıştığınızı varsayar. ids; Eğer durum böyle değilse o zaman kullanmak istersiniz connection.quote ya da ActiveRecord SQL dezenfektan yöntemleri doğru şekilde alıntı yapmak ids.

Sonra kullan order sipariş koşulu olarak dize:

Object.find(ids, :order => order)

ya da modern dünyada:

Object.where(:id => ids).order(order)

Bu biraz ayrıntılı, ancak herhangi bir SQL veritabanı ile aynı şekilde çalışmalı ve çirkinliği gizlemek zor değil.


6
2018-03-22 07:08



Ben yaptım Object.order('case id ' + ids.each_with_index.map { |id, i| "when #{id} then #{i}" }.join(' ') + ' end') - Peter Hamilton
Peter'ın yaptığı gibi mu? +1 - dj.
veya sql enjeksiyon kaçışına, bunu harita bloğuna yerleştirin: sanitize_sql_array(["when ? then ? ", id, i]) (AR modelinde çalışır) - nruth
@nruth: Seçilmemiş bir varsayım vardı. ids İçerilen rakamlar, varsayımlar kötü bir fikirdir (açık olmadıkları sürece iki katına çıkarlar) bu yüzden umarım bunu temizledim. - mu is too short
Yine de, özellikle 2013'te iyi bir cevap ve makul bir varsayımdır. Ancak, javascript ön yüzleri ve modaya uygun uuids ile Google, sipariş sorununu çözmek için burada belirttiği gibi bir söze değinmiş gibi görünüyor. - nruth


Cevapladığım gibi İşteAz önce bir mücevher çıkardımorder_as_specified) bu gibi yerel SQL siparişini yapmanızı sağlar:

Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])

Sadece test edildi ve SQLite'de çalışıyor.


4
2018-03-13 18:30





Justin Weiss yazdı bu sorunla ilgili blog yazısısadece iki gün önce.

Veritabanına tercih edilen sipariş hakkında bilgi vermek ve bu sıraya göre sıralanmış tüm kayıtları doğrudan veritabanından yüklemek iyi bir yaklaşım gibi görünmektedir. Onun örnek blog makalesi:

# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      order_clause = "CASE id "
      ids.each_with_index do |id, index|
        order_clause << "WHEN #{id} THEN #{index} "
      end
      order_clause << "ELSE #{ids.length} END"
      where(id: ids).order(order_clause)
    end
  end
end

ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)

Bu yazmanıza olanak tanır:

Object.find_ordered([2, 1, 3]) # => [2, 1, 3]

3
2018-04-22 00:57



Nice drop-in çözüm ama sadece geçerli id. Siparişe göre niteliği dinamik olarak belirleyebilmek güzel olurdu uid ya da başka birşey. - Joshua Pinter
@JoshPinter: Sadece bir parametre ekle field_name yönteme ve field_name yerine "CASE id" bunun gibi: "CASE #{field_name} " - spickermann
Evet, takip için teşekkürler. Daha fazla düşünmeyi düşünüyordum find_ordered_by_uid(uids) ama sadece söylediklerinizi inşa ediyor. Şerefe. - Joshua Pinter
Bu kod boş dizilerde kırılıyor ... her neyse ... - alexandrecosta
Yöntemin adını değiştirmeyi öner find_by_ordered_ids daha açık olmak ve modül dosyasının adını eşleştirmek için. - Joshua Pinter


Ruby'de yapmak için bir başka (muhtemelen daha verimli) yolu:

ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record| 
  result[record.id] = record
  result
end
sorted_records = ids.map {|id| records_by_id[id] }

1
2017-08-24 11:00



Bu kodu iki satır FWIW yapmak için her_with_object öğesini kullanabilirsiniz. ayrıca bazı kıyaslamalar yaptı ve bu kodun çok Büyük bir set üzerinde yineleme yapıyorsanız daha hızlı. - Schneems
Yorumun için teşekkürler. #Each_with_object'in varlığını bilmiyordum (api.rubyonrails.org/classes/...). Yukarıdaki #inject ve #each_with_object yaklaşımı arasında önemli bir fark var mı? - Jeroen van Dijk
Nope, her_with_object aslında, değiştirdiğiniz nesneyi döndürmeniz gerekmediği için, enjekte etme ile aynıdır (sizin durumunuzda) result) bloğunuzun sonunda. Yapı metali IMHO'yu basitleştirir. - Schneems


İşte yapabileceğim en basit şey:

ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }

1
2018-06-04 21:08