martin
martin

Reputation: 3559

Sort list of objects according to an array (Rails)

I have an array if ids from a table e.g:

b = [659, 658, 656, 645, 644, 657, 634, 643, 649, 650, 651, 636, 633, 607, 605, 604, 648, 647, 675, 674, 667, 499]

I use it for retrieving a list of objects from tat table:

k = Photo.where(id:b)

What I get is a list of objects which are NOT sorted in the same order as the array.

How can I do that?

Upvotes: 2

Views: 2132

Answers (3)

Ishtiaque05
Ishtiaque05

Reputation: 451

b.map { |m| Photo.find(m) } if the ID's in b array are unique. If not then run this b.uniq.map { |m| Photo.find(m) } One drawback of this code is it runs a query for every elements present inside b array.

Upvotes: 0

Eric Duminil
Eric Duminil

Reputation: 54223

If ids are unique

You can use index_by and values_at :

k = Photo.where(id: b).index_by(&:id).values_at(*b)

Example :

b = [5,3,1]
Country.where(id: b)
#=> [#<Country id: 1, name: "France">, #<Country id: 3, name: "Ukraine">,  #<Country id: 5, name: "Spain">]
Country.where(id: b).index_by(&:id)
#=> {1=>#<Country id: 1, name: "France">, 3=>#<Country id: 3, name: "Ukraine">, 5=>#<Country id: 5, name: "Spain">}
Country.where(id: b).index_by(&:id).values_at(*b)
#=> [#<Country id: 5, name: "Spain">, #<Country id: 3, name: "Ukraine">, #<Country id: 1, name: "France">]

If b contains duplicate ids

With b = [5,3,3,1], the first method would output :

#=> [#<Country id: 5, name: "Spain">, #<Country id: 3, name: "Ukraine">, #<Country id: 3, name: "Ukraine">, #<Country id: 1, name: "France">]

You can use index_by and slice:

k = Photo.where(id: b).index_by(&:id).slice(*b).values

With :

b = [5,3,3,1]
Country.where(id: b)
#=> [#<Country id: 1, name: "France">, #<Country id: 3, name: "Ukraine">,  #<Country id: 5, name: "Spain">]
Country.where(id: b).index_by(&:id)
#=> {1=>#<Country id: 1, name: "France">, 3=>#<Country id: 3, name: "Ukraine">, 5=>#<Country id: 5, name: "Spain">}
Country.where(id: b).index_by(&:id).slice(*b)
#=> {5=>#<Country id: 5, name: "Spain">, 3=>#<Country id: 3, name: "Ukraine">, 1=>#<Country id: 1, name: "France">}
Country.where(id: b).index_by(&:id).slice(*b).values
#=> [#<Country id: 5, name: "Spain">, #<Country id: 3, name: "Ukraine">, #<Country id: 1, name: "France">]

Of course, you could also use the first method with b.uniq

Upvotes: 5

mu is too short
mu is too short

Reputation: 434665

An ActiveRecord query like Photo.where(id: some_array) produces SQL like this:

select ... from photos where id in (...)

but the order of elements in the IN has no relation to what order the database returns the rows in; in general, the only thing that determines the order of the rows is the ORDER BY clause.

You can get the database to order things to match your b array with a CASE:

order by case id when 659 then 0 when 658 then 1 ... end

and that's fairly easily to construct in Ruby:

Photo.where(id: b)
     .order(%Q{case id #{b.map.each_with_index { |id, i| "when #{id} then #{i}" }.join(' ')} end })

Not terribly pretty I suppose but it gets the database to do the work (which might be important if you're chaining more things into this query) and you can easily hide the ugliness inside a class method on Photo:

def self.for_ids_in_order(ids)
  where(id: ids).order(%Q{case id #{ids.map.each_with_index { |id, i| "when #{id} then #{i}" }.join(' ')} end })
end

and then say Photo.for_ids_in_order(b).

Of course, this does assume that you know where the b array came from and that you know it contains integers. If you're not certain of that then you can throw a connection.quote call in to make sure things are properly escaped:

ids.map.each_with_index { |id, i| "when #{connection.quote(id)} then #{i}" }...

Upvotes: 1

Related Questions