ActiveRecord.find(array_of_ids), preserving order

Viewed 53228

When you do Something.find(array_of_ids) in Rails, the order of the resulting array does not depend on the order of array_of_ids.

Is there any way to do the find and preserve the order?

ATM I manually sort the records based on order of IDs, but that is kind of lame.

UPD: if it's possible to specify the order using the :order param and some kind of SQL clause, then how?

15 Answers

There is a gem find_with_order which allows you to do it efficiently by using native SQL query.

And it supports both Mysql and PostgreSQL.

For example:

Something.find_with_order(array_of_ids)

If you want relation:

Something.where_with_order(:id, array_of_ids)

Assuming Model.pluck(:id) returns [1,2,3,4] and you want the order of [2,4,1,3]

The concept is to to utilize the ORDER BY CASE WHEN SQL clause. For example:

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;

In Rails, you can achieve this by having a public method in your model to construct a similar structure:

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

Then do:

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

The good thing about this. Results are returned as ActiveRecord Relations (allowing you to use methods like last, count, where, pluck, etc)

Although I don't see it mentioned anywhere in a CHANGELOG, it looks like this functionality was changed with the release of version 5.2.0.

Here commit updating the docs tagged with 5.2.0 However it appears to have also been backported into version 5.0.

With reference to the answer here

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") works for Postgresql.

I liked the order based options a lot. My 2c is that it makes sense to add it in a scope, so that you can use it chained with other AR methods

scope :find_in_order, ->(ids) { 
  where(id: ids).order([Arel.sql('FIELD(id, ?)'), ids])
}
Related