指定された順序でidによってActiveRecordオブジェクトを見つけるクリーンな方法
-
03-07-2019 - |
質問
idの配列を指定してActiveRecordオブジェクトの配列を取得したい。
仮定しました
Object.find([5,2,3])
オブジェクト5、オブジェクト2、オブジェクト3の順序で配列を返しますが、代わりにオブジェクト2、オブジェクト3、オブジェクト5の順に配列を取得します。
ActiveRecord Base findメソッドAPI は、すべきではないことを述べています指定された順序で期待してください(他のドキュメントではこの警告が出されません)。
1つの潜在的なソリューションがで提供されました。同じIDの配列で検索注文?、しかし注文オプションはSQLiteに有効ではないようです。
自分でオブジェクトをソートするルビーコードを書くこともできますが(多少シンプルでスケーリングが不十分か、スケーリングが改善され、より複雑になります)、より良い方法はありますか?
解決
MySQLや他のDBがそれ自体をソートするのではなく、ソートしないということです。 Model.find([5、2、3])
を呼び出すと、生成されるSQLは次のようになります。
SELECT * FROM models WHERE models.id IN (5, 2, 3)
これは順序を指定するのではなく、返されるレコードのセットのみを指定します。一般に、MySQLはデータベースの行を 'id'
の順序で返しますが、これは保証されていません。
データベースが保証された順序でレコードを返すようにする唯一の方法は、order句を追加することです。レコードが常に特定の順序で返される場合、dbにソート列を追加して、 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}}
他のヒント
Jeroen van Dijkへの以前のコメントに基づいて、 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
今、もう1つ
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]
どうやらmySQLと他のDB管理システムは、それ自体をソートします。私はあなたがそのことをバイパスできると思います:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
ポータブルなソリューションは、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
で数値またはその他の安全な値を操作していることを前提としています。そうでない場合は、 <を使用します。 code> connection.quote または ActiveRecord SQLサニタイザーメソッドを使用して、 ids
を適切に引用します。
次に、注文条件として order
文字列を使用します。
Object.find(ids, :order => order)
または現代の世界:
Object.where(:id => ids).order(order)
これは少し冗長ですが、どのSQLデータベースでも同じように機能するはずです。さを隠すのはそれほど難しくありません。
こちらと回答したところ、gemをリリースしました( order_as_specified )を使用すると、次のようにネイティブSQLの順序付けを行うことができます。
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
テスト済みで、SQLiteで動作します。
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]
これは、メソッドとしての1行(パフォーマー(detectのようなO(n)配列検索ではなく、ハッシュ検索)です):
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
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] }
これは、私が思いつく最も簡単なものです。
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}
これはおそらく最も簡単な方法です。検索するオブジェクトが多すぎないと仮定すると、各IDに対してデータベースにアクセスする必要があるためです。