ActiveRecord.find (array_of_ids), koruma düzeni


100

Rails'de yaptığınızda Something.find(array_of_ids), ortaya çıkan dizinin sırası, sırasına bağlı değildir array_of_ids.

Düzeni bulmanın ve korumanın bir yolu var mı?

ATM Kayıtları kimlik sırasına göre manuel olarak sıralıyorum, ancak bu biraz yetersiz.

UPD: sırayı :orderparam ve bir tür SQL cümlesi kullanarak belirtmek mümkünse , o zaman nasıl?


Yanıtlar:


71

Cevap sadece mysql içindir

Mysql'de FIELD () adında bir işlev var

Bunu .find () içinde nasıl kullanabileceğiniz aşağıda açıklanmıştır:

>> 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 ','})")

Güncelleme: Bu, Rails 6.1 Rails kaynak kodundan kaldırılacaktır.


FIELDS()Postgres ile eşdeğerini biliyor musunuz ?
Trung Lê

3
Bunu postgres'de yapmak için bir plpgsql işlevi yazdım - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi

25
Bu artık çalışmıyor. Daha yeni Rails için:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff

2
Bu, Gunchars'tan daha iyi bir çözüm çünkü sayfalandırmayı bozmayacak.
pguardiario

.ids benim için iyi çalışıyor ve oldukça hızlı activerecord belgeleri
TheJKFever

79

İşin garibi, hiç kimse böyle bir şey önermedi:

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

SQL arka ucunun yapmasına izin vermenin yanı sıra olabildiğince verimli.

Düzenleme: Kendi cevabımı geliştirmek için, bunu şu şekilde de yapabilirsiniz:

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

#index_byve ActiveSupport'ta sırasıyla diziler ve karmalar için #sliceoldukça kullanışlı eklemelerdir .


Öyleyse düzenlemeniz işe yarıyor gibi görünüyor, ancak bir hash içindeki anahtar sıralaması beni sinirlendiriyor, garantili değil mi? böylelikle dilim çağırıp "yeniden sıralı" hash'i geri aldığınızda, bu gerçekten anahtarların eklendiği sırayla hash döndüren değerlere bağlıdır. Bu, değişebilecek bir uygulama detayına bağlı gibi geliyor.
Jon

2
@Jon, sıra Ruby 1.9'da ve onu izlemeye çalışan diğer tüm uygulamalarda garantilidir. 1.8 için Rails (ActiveSupport), Hash sınıfının aynı şekilde davranmasını sağlamak için yama uygular, bu nedenle Rails kullanıyorsanız, iyi olmalısınız.
Gunchars

açıklama için teşekkürler, sadece belgelerde buldum.
Jon

13
Bununla ilgili sorun, bir ilişki yerine bir dizi döndürmesidir.
Velizar Hristov

3
Harika, ancak tek astar benim için çalışmıyor (Rails 4.1)
Besi

44

As Mike Woodhouse belirtilen onun cevabı bu başlık altında, Raylar a sahip bir SQL sorgusu kullanıyor, becase oluşurWHERE id IN... clause tek sorguda kayıtların tümünü almak için. Bu, her bir kimliği ayrı ayrı almaktan daha hızlıdır, ancak fark ettiğiniz gibi, aldığınız kayıtların sırasını korumaz.

Bunu düzeltmek için, kayıtlara bakarken kullandığınız orijinal kimlik listesine göre uygulama düzeyinde kayıtları sıralayabilirsiniz.

Bir diziyi başka bir dizinin elemanlarına göre sıralamak için verilen birçok mükemmel cevaba dayanarak , aşağıdaki çözümü öneriyorum:

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

Ya da biraz daha hızlı bir şeye ihtiyacınız varsa (ancak muhtemelen biraz daha az okunabilir), bunu yapabilirsiniz:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)

3
ikinci çözüm (index_by ile) benim için başarısız görünüyor ve tüm sıfır sonuçları veriyor.
Ben Wheeler

22

Bu postgresql ( kaynak ) için çalışıyor gibi görünüyor - ve bir ActiveRecord ilişkisi döndürüyor

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])

diğer sütunlar için de genişletilebilir, örneğin namesütun için, şunu kullanın position(name::text in ?)...


Sen benim haftanın kahramanısın Teşekkür ederim!
ntdb

4
Bunun yalnızca önemsiz durumlarda işe yaradığını unutmayın, sonunda kimliğinizin listedeki diğer kimliklerin içinde bulunduğu bir durumla karşılaşacaksınız (örneğin 11'de 1'i bulacaktır). Bunu aşmanın bir yolu, virgülleri konum kontrolüne eklemek ve ardından birleştirmeye şu şekilde bir son virgül eklemektir: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy

İyi nokta, @IrishDubGuy! Cevabımı önerinize göre güncelleyeceğim. Teşekkürler!
gingerlime

benim için zincirleme çalışmıyor. Burada tablo adı, id: metinden önce eklenmelidir: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] benim için çalışan tam sürüm: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } teşekkürler @gingerlime @IrishDubGuy
user1136228

Sanırım bazı birleşimler yapmanız durumunda tablo adını eklemeniz gerekiyor ... Bu, katıldığınızda ActiveRecord kapsamlarında oldukça yaygındır.
gingerlime

19

Ben cevap olarak burada , ben sadece bir mücevher (serbest order_as_specified böyle yerel SQL sipariş yapmanızı sağlar):

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

Test edebildiğim kadarıyla, tüm RDBMS'lerde yerel olarak çalışıyor ve zincirlenebilen bir ActiveRecord ilişkisi döndürüyor.


1
Ahbap, harikasın. Teşekkür ederim!
swrobel

5

Her durumda işe yarayan SQL'de ne yazık ki mümkün değil, ya her kayıt için tek bir bulgu yazmanız gerekecek ya da Ruby'de sıralamayı özel teknikler kullanarak çalıştırmanın bir yolu olabilir:

İlk örnek:

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

ÇOK YETERSİZ

İkinci örnek:

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

Çok verimli olmasa da, bu geçici çözümün DB'den bağımsız olduğunu ve az miktarda satırınız varsa kabul edilebilir olduğunu kabul ediyorum.
Trung Lê

Bunun için enjekte etme, bu bir harita:sorted = arr.map { |val| Model.find(val) }
tokland

ilki yavaş. İkincisine şu şekilde katılıyorum:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

2

@Gunchars cevabı harika, ancak Hash sınıfı sipariş edilmediğinden Rails 2.3'te kutudan çıkmıyor. Basit bir çözüm, Enumerable sınıfını index_byOrderedHash sınıfını kullanacak şekilde genişletmektir:

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

Şimdi @ Gunchars'ın yaklaşımı işe yarayacak

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

Sonra

Something.find_with_relevance(array_of_ids)

2

Varsayarsak Model.pluck(:id)getiri [1,2,3,4]ve sırasını istediğiniz[2,4,1,3]

Kavram, ORDER BY CASE WHENSQL cümlesini kullanmaktır . Örneğin:

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;

Rails'de, benzer bir yapı oluşturmak için modelinizde genel bir yönteme sahip olarak bunu başarabilirsiniz:

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

O zaman yap:

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

Bununla ilgili iyi olan şey. Sonuçlar ActiveRecord İlişkiler olarak döndürülür (sizin gibi yöntemleri kullanmak için izin last, count, where, pluck, vs)


2

Gem find_with_order varYerel SQL sorgusunu kullanarak bunu verimli bir şekilde yapmanızı sağlayan vardır.

Ve bu destekler hem MysqlvePostgreSQL .

Örneğin:

Something.find_with_order(array_of_ids)

İlişki istiyorsanız:

Something.where_with_order(:id, array_of_ids)

1

Kaputun altında, findbir dizi kimlikle, SELECTbirWHERE id IN... döngü yapmaktan daha verimli olması gereken cümleci oluşturur.

İstek veritabanına bir seyahat halinde memnun, ancak Yani SELECTolmadan ler ORDER BYmaddeleri sıralanmamış bulunmaktadır. ActiveRecord bunu anlıyor, bu nedenle findaşağıdaki gibi genişletiyoruz :

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

Dizinizdeki kimliklerin sırası gelişigüzel ve önemliyse (yani, döndürülen satırların sırasının, içerdiği kimliklerin sırasına bakılmaksızın dizinizle eşleşmesini istiyorsanız), o zaman sonuçları son işlemden geçirerek en iyi sunucu olacağını düşünüyorum. kod - bir :ordercümle oluşturabilirsiniz, ancak bu son derece karmaşık olacaktır ve hiç de niyeti açığa çıkarmayacaktır.


Seçenekler karmasının kullanımdan kaldırıldığını unutmayın. (bu örnekte ikinci argüman :order => id)
ocodo

1

CHANGELOG'da herhangi bir yerde bahsedildiğini görmesem de, bu işlevsellik sürümün yayınlanmasıyla değişmiş gibi görünüyor5.2.0 .

İşte taahhüt docs güncellenmesi ile etiketlenmiş 5.2.0aynı zamanda gibi görünüyor Ancak backported sürüm içine 5.0.


0

Cevap atıfla burada

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") Postgresql için çalışıyor.


-4

Find (: order => '...') içinde kayıtları getirirken bunu yapan bir sipariş cümlesi vardır. Buradan da yardım alabilirsiniz.

bağlantı metni

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.