web-dev-qa-db-ja.com

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

RailsでSomething.find(array_of_ids)を実行すると、結果の配列の順序はarray_of_idsの順序に依存しません。

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

ATM私はIDの順序に基づいてレコードを手動でソートしますが、それは一種のラメです。

UPD::order paramと何らかのSQL句を使用して順序を指定できる場合、どのようになりますか?

92
Leonid Shevtsov

答えは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 ','})")
69
kovyrin

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

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および#sliceは、それぞれ配列とハッシュの ActiveSupport に非常に便利な追加です。

76
Gunchars

Mike Woodhouse彼の答え で述べられているように、これは、フードの下でRailsがWHERE id IN... clause 1つのクエリですべてのレコードを取得します。これは、各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)
37
Ajedi32

これはpostgresqlsource )で動作するようで、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 ?)を使用します...

20
gingerlime

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

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

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

18
JacobEvelyn

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

最初の例:

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}}
5
Omar Qureshi

@Guncharsの答えは素晴らしいですが、Rails 2.3では箱から出して機能しません。Hashクラスが順序付けられていないためです。簡単な回避策は、Enumerableクラスを拡張することですindex_byは、OrderedHashクラスを使用します。

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
Chris Bloom

Model.pluck(:id)[1,2,3,4]を返し、[2,4,1,3]の順序が必要だと仮定します

概念は、ORDER BY CASE WHEN SQL句を利用することです。例えば:

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 Relationsとして返されます(lastcountwherepluckなどのメソッドを使用できます)

2

内部では、IDの配列を持つfindは、WHERE id IN...句を持つSELECTを生成します。これは、IDをループするよりも効率的です。

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

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

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

1
Mike Woodhouse

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

また、MysqlPostgreSQLの両方をサポートしています。

例えば:

Something.find_with_order(array_of_ids)

関係が必要な場合:

Something.where_with_order(:id, array_of_ids)
1
khiav reoy

CHANGELOGのどこにも言及されていませんが、バージョン5.2.0のリリースでは、この機能 変更された のように見えます。

ここで commit5.2.0でタグ付けされたドキュメントを更新していますが、 backported がバージョン5.0になっているようです。

1
XML Slayer

答えを参照して here

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')")はPostgresqlで機能します。

0
Sam Kah Chiin