ActiveRecord.find(array_of_ids)、順序を保持


98

Something.find(array_of_ids)Railsで行う場合、結果の配列の順序はの順序に依存しませんarray_of_ids

注文を見つけて保存する方法はありますか?

ATM私はIDの順序に基づいて手動でレコードをソートしますが、それはちょっと不自然です。

UPD::orderparamとなんらかのSQL句を使用して順序を指定できる場合は、どうすればよいですか?


回答:


71

答えはmysqlのみです

mysqlにはFIELD()という関数があります

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

更新:これはRails 6.1 Railsソースコードで削除されます


FIELDS()Postgres の同等の機能を知っていますか?
TrungLê12年


25
これはもう機能しません。最近のRailsの場合:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff '18

2
これは、ページネーションを壊さないので、ガンチャーズよりも優れたソリューションです。
pguardiario 2015年

.idsは私のために罰金を動作し、非常に高速であるActiveRecordのドキュメント
TheJKFever

79

奇妙なことに、誰もこのようなことを提案していません:

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

SQLバックエンドにそれをさせることを除いてそれが得るのと同じくらい効率的です。

編集:私自身の答えを改善するために、次のようにすることもできます:

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

#index_byおよび配列とハッシュのActiveSupportでそれぞれ#sliceかなり便利な追加です。


だからあなたの編集は機能しているようですが、ハッシュでの緊張したキーの順序になります保証されていませんか?そのため、sliceを呼び出してハッシュを「再配列」して戻す場合、それは実際には、キーが追加された順序でハッシュが値を返すことに依存しています。これは、変更される可能性がある実装の詳細に依存しているように感じます。
Jon

2
@ジョン、順序はRuby 1.9とそれに従うことを試みる他のすべての実装で保証されています。1.8の場合、Rails(ActiveSupport)はHashクラスにパッチを適用して同じように動作させるため、Railsを使用している場合は問題ありません。
Gunchars 2013

説明をありがとう、ちょうどドキュメントでそれを見つけました。
Jon

13
これの問題は、リレーションではなく配列を返すことです。
Velizar Hristov 2015

3
素晴らしいですが、ワンライナーは私には機能しません(Rails 4.1)
Besi

44

マイク・ウッドハウスの中で述べた彼の答えは、これはbecase発生し、ボンネットの下に、RailsはとSQLクエリを使用しているWHERE id IN... clause1つのクエリですべてのレコードを取得するために。これは、各IDを個別に取得するよりも高速ですが、気づいたように、取得するレコードの順序は保持されません。

これを修正するために、レコードを検索するときに使用したIDの元のリストに従って、アプリケーションレベルでレコードをソートできます。

別の配列の要素に従って配列並べ替える多くの優れた回答に基づいて、次の解決策をお勧めします。

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

または、少し高速なものが必要な場合(ただし、おそらく多少読みにくくなります)、これを行うことができます。

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

3
2番目のソリューション(index_byを使用)は失敗するようで、すべての結果がnilになります。
Ben Wheeler

22

これはpostgresqlソース)で機能するようです-そして、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])

他のカラムにも拡張できます。たとえば、nameカラムにはposition(name::text in ?)...


あなたは私の今週のヒーローです。ありがとうございました!
ntdb 2016

4
これは些細な場合にのみ機能することに注意してください。最終的に、IDがリスト内の他のIDに含まれている状況に遭遇します(たとえば、11に1が見つかります)。これを回避する1つの方法は、次のように、位置チェックにコンマを追加し、最後のコンマを結合に追加することです。order = sanitize_sql_array(["position( '、' || clients.id :: text || '、 'in?) "、ids.join('、 ')+'、 '])
IrishDubGuy

良い点、@ IrishDubGuy!私はあなたの提案に基づいて私の答えを更新します。ありがとう!
ジンジャーライム2016年

私にとって連鎖は機能しません。ここで、テーブル名は次のようにid:textの前に追加する必要があります。["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ます:私のために機能したフルバージョン:scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } Thanks @gingerlime @IrishDubGuy
user1136228

結合を行う場合に備えて、テーブル名を追加する必要があると思います...これは、結合するときのActiveRecordスコープではよくあることです。
gingerlime 2017年

19

私がここで答えように、次のようなネイティブSQLの順序付けを可能にするgem(order_as_specified)をリリースしました。

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

私がテストできた限り、それはすべてのRDBMSでネイティブに動作し、連鎖可能なActiveRecordリレーションを返します。


1
おい、あなたはとても素晴らしいです。ありがとうございました!
swrobel、2016年

5

残念ながらすべてのケースで機能するSQLでは不可能です。独自の手法を使用して機能させる方法がおそらくあるかもしれませんが、ルビで各レコードまたは注文ごとに単一の検索結果を書き込む必要があります。

最初の例:

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

非常に非効率的

2番目の例:

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

あまり効率的ではありませんが、この回避策はDBに依存せず、行数が少ない場合は許容できることに同意します。
TrungLê

このためいけない」使用注入は、それはマップです:sorted = arr.map { |val| Model.find(val) }
tokland

最初は遅いです。私は次のようなマップで2番目のものに同意します:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

2

@Guncharsの回答はすばらしいですが、Hashクラスが順序付けされていないため、Rails 2.3ではそのままでは機能しません。簡単な回避策は、Enumerableクラスを拡張してindex_byOrderedHashクラスを使用することです。

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

これで、@ Guncharsのアプローチが機能します

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

ボーナス

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

その後

Something.find_with_relevance(array_of_ids)

2

仮定すると、Model.pluck(:id)リターンを[1,2,3,4]、あなたはの順序をしたいです[2,4,1,3]

コンセプトは、ORDER BY CASE WHENSQL句を利用することです。例えば:

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では、モデルにパブリックメソッドを配置して同様の構造を構築することで、これを実現できます。

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

次に行います:

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

これの良いところ。結果は(あなたのようなメソッドを使用できるようにActiveRecordの関係として返されlastcountwherepluck、など)


2

宝石find_with_orderがありますネイティブSQLクエリを使用して効率的に実行できるます。

との両方MysqlをサポートしていPostgreSQLます。

例えば:

Something.find_with_order(array_of_ids)

関係が必要な場合:

Something.where_with_order(:id, array_of_ids)

1

ボンネットの下で、findIDの配列で生成されますSELECTとをWHERE id IN... IDSをループよりも効率的でなければならない句。

したがって、データベースへの1回の移動で要求は満たされますが、句SELECTのないs ORDER BYはソートされません。ActiveRecordはこれを理解するため、次のように拡張findします。

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

配列内のIDの順序が任意で重要である場合(つまり、そこに含まれるIDのシーケンスに関係なく、返される行の順序が配列と一致するようにする場合)、コード- :order句を作成することはできますが、それは非常に複雑で、意図を明らかにすることはできません。


オプションハッシュは廃止されていることに注意してください。(この例では、2番目の引数:order => id
ocodo 2012



弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.