29

IDの配列を指定してActiveRecordオブジェクトの配列を取得したいと思います。

私は

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

オブジェクト5、オブジェクト2、オブジェクト3の順に配列を返しますが、代わりに、オブジェクト2、オブジェクト3、オブジェクト5の順に配列を取得します。

ActiveRecord BaseのfindメソッドAPIは、提供された順序でそれを期待するべきではないと述べています(他のドキュメントではこの警告は示されていません)。

1つの潜在的な解決策は、同じ順序のIDの配列による検索で与えられましたか?、しかし、注文オプションはSQLiteでは有効ではないようです。

オブジェクトを自分でソートするためのルビーコードを書くことはできますが(やや単純でスケーリングが不十分であるか、スケーリングがより適切でより複雑です)、より良い方法はありますか?

4

10 に答える 10

23

MySQL やその他の DB が独自にソートするのではなく、ソートしないということです。を呼び出すとModel.find([5, 2, 3])、次のような SQL が生成されます。

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

これは順序を指定するのではなく、返されるレコードのセットのみを指定します。通常、MySQL はデータベースの行を'id'順番に返しますが、これが保証されるわけではありません。

データベースが保証された順序でレコードを返すようにする唯一の方法は、順序句を追加することです。レコードが常に特定の順序で返される場合は、並べ替え列をデータベースに追加してModel.find([5, 2, 3], :order => 'sort_column'). そうでない場合は、コードで並べ替えを行う必要があります。

ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}} 
于 2009-04-30T07:41:30.013 に答える
10

Jeroen van Dijkへの私の以前のコメントに基づいて、これをより効率的に、2行で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]}

参考までに、私が使用したベンチマークです

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

今もう一方

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

アップデート

これは、ほとんどの場合、orderステートメントとcaseステートメントを使用して実行できます。これは、使用できるクラスメソッドです。

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]
于 2011-09-13T19:19:19.397 に答える
7

どうやら、mySQL やその他の DB 管理システムは独自にソートを行っているようです。私はあなたがそれをバイパスできると思います:

ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
于 2009-04-29T16:13:19.853 に答える
6

移植可能な解決策は、ORDER BY で SQL CASE ステートメントを使用することです。ORDER BY ではほとんどすべての式を使用でき、CASE はインラインのルックアップ テーブルとして使用できます。たとえば、目的の SQL は次のようになります。

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

これは、Ruby を少し使えば非常に簡単に生成できます。

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

上記は、 で数値またはその他の安全な値を扱っていることを前提としていますids。そうでない場合は、またはActiveRecord SQLサニタイザーメソッドconnection.quoteの1つを使用して、.ids

次に、order文字列を注文条件として使用します。

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

または現代の世界では:

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

これは少し冗長ですが、どの SQL データベースでも同じように機能するはずであり、醜さを隠すのはそれほど難しくありません。

于 2013-03-22T07:08:27.857 に答える
4

ここで回答したように、次のようなネイティブ SQL 順序付けを行うことができるgem ( order_as_specified ) をリリースしました。

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

テストしたばかりで、SQLiteで動作します。

于 2015-03-13T18:30:41.160 に答える
3

Justin Weiss は、この問題について2 日前にブログ記事を書きました。

データベースに優先順序を伝え、その順序でソートされたすべてのレコードをデータベースから直接ロードするのは良い方法のようです。彼のブログ記事の例:

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

これにより、次のように記述できます。

Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
于 2015-04-22T00:57:56.803 に答える
1

Rubyでそれを行う別の(おそらくより効率的な)方法:

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] }
于 2011-08-24T11:00:35.157 に答える
1

これが私が思いつくことができる最も簡単なことです:

ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
于 2012-06-04T21:08:04.903 に答える
0
@things = [5,2,3].map{|id| Object.find(id)}

ID ごとにデータベースに移動する必要があるため、検索するオブジェクトが多すぎないと仮定すると、これがおそらく最も簡単な方法です。

于 2009-04-29T16:44:37.423 に答える