Sort list of objects according to an array (Rails)

时间秒杀一切 提交于 2019-12-06 14:21:44

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

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}" }...

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!