ActiveRecord.find(array_of_ids), preserving order
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?
Solution 1:
Oddly, no one has suggested something like this:
index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }
As efficient as it gets besides letting SQL backend do it.
Edit: To improve on my own answer, you can also do it like this:
Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
#index_by
and #slice
are pretty handy additions in ActiveSupport for arrays and hashes respectively.
Solution 2:
The answer is for mysql only
There is a function in mysql called FIELD()
Here is how you could use it in .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 ','})")
Update: This will be removed in Rails 6.1 Rails source code
Solution 3:
As Mike Woodhouse stated in his answer, this occurs becase, under the hood, Rails is using an SQL query with a WHERE id IN... clause
to retrieve all of the records in one query. This is faster than retrieving each id individually, but as you noticed it doesn't preserve the order of the records you are retrieving.
In order to fix this, you can sort the records at the application level according to the original list of IDs you used when looking up the record.
Based on the many excellent answers to Sort an array according to the elements of another array, I recommend the following solution:
Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}
Or if you need something a bit faster (but arguably somewhat less readable) you could do this:
Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Solution 4:
This seems to work for postgresql (source) - and returns an ActiveRecord relation
class Something < ActiveRecrd::Base
scope :for_ids_with_order, ->(ids) {
order = sanitize_sql_array(
["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
)
where(:id => ids).order(Arel.sql(order))
}
end
# usage:
Something.for_ids_with_order([1, 3, 2])
can be extended for other columns as well, e.g. for the name
column, use position(name::text in ?)
...
Solution 5:
As I answered here, I just released a gem (order_as_specified) that allows you to do native SQL ordering like this:
Something.find(array_of_ids).order_as_specified(id: array_of_ids)
As far as I've been able to test, it works natively in all RDBMSes, and it returns an ActiveRecord relation that can be chained.