我想获得一个给定一组id的ActiveRecord对象数组.
我认为
Object.find([5,2,3])
将返回一个数组,其中包含对象5,对象2,然后按顺序返回对象3,但我得到的数组按对象2,对象3和对象5排序.
ActiveRecord Base 查找方法API提到您不应该按照提供的顺序期望它(其他文档不提供此警告).
一个可能的解决方案是按相同顺序的ID数组查找的?,但订单选项似乎对SQLite无效.
我可以编写一些ruby代码来自己对对象进行排序(有点简单,缩放比例较差或缩放比较复杂),但是有更好的方法吗?
MySQL和其他数据库并不是自己排序的,而是他们不对它们进行排序.当您调用时Model.find([5, 2, 3])
,生成的SQL类似于:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
这不指定订单,只指定要返回的记录集.事实证明,通常MySQL会按'id'
顺序返回数据库行,但不能保证这一点.
使数据库以保证顺序返回记录的唯一方法是添加一个order子句.如果您的记录将始终按特定顺序返回,那么您可以向db添加排序列并执行Model.find([5, 2, 3], :order => 'sort_column')
.如果不是这种情况,您将不得不在代码中进行排序:
ids = [5, 2, 3] records = Model.find(ids) sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
基于我之前对Jeroen van Dijk的评论,您可以更有效地使用两行进行此操作 each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result } ids.map {|id| result_hash[id]}
这里参考我使用的基准
ids = [5,3,1,4,11,13,10] results = Model.find(ids) Benchmark.measure do 100000.times do result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result } ids.map {|id| result_hash[id]} end end.real #=> 4.45757484436035 seconds
现在是另一个
ids = [5,3,1,4,11,13,10] results = Model.find(ids) Benchmark.measure do 100000.times do ids.collect {|id| results.detect {|result| result.id == id}} end end.real # => 6.10875988006592
更新
您可以在大多数情况下使用order和case语句执行此操作,这是您可以使用的类方法.
def self.order_by_ids(ids) order_by = ["case"] ids.each_with_index.map do |id, index| order_by << "WHEN id='#{id}' THEN #{index}" end order_by << "end" order(order_by.join(" ")) end # User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) # #=> [3,2,1]
显然,mySQL和其他数据库管理系统可以自行排序.我认为你可以绕过这样做:
ids = [5,2,3] @things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
可移植的解决方案是在ORDER BY中使用SQL CASE语句.您可以在ORDER BY中使用几乎任何表达式,CASE可以用作内联查找表.例如,您之后的SQL将如下所示:
select ... order by case id when 5 then 0 when 2 then 1 when 3 then 2 end
用一点Ruby生成它很容易:
ids = [5, 2, 3] order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
以上假设您正在处理数字或其他一些安全值ids
; 如果不是这种情况,那么你想要使用connection.quote
或者使用一种ActiveRecord SQL清理程序方法来正确引用你的ids
.
然后使用order
字符串作为您的订购条件:
Object.find(ids, :order => order)
或者在现代世界:
Object.where(:id => ids).order(order)
这有点冗长,但它应该对任何SQL数据库都一样,并且隐藏丑陋并不困难.