29

我想获得一个给定 id 数组的 ActiveRecord 对象数组。

我以为

Object.find([5,2,3])

将返回一个包含对象 5、对象 2、然后是对象 3 的数组,但我得到的数组顺序为对象 2、对象 3 和对象 5。

ActiveRecord Base find 方法 API提到您不应该按提供的顺序期望它(其他文档没有给出此警告)。

一个潜在的解决方案是在Find by ids array 中以相同的顺序给出的?,但 order 选项似乎对 SQLite 无效。

我可以编写一些 ruby​​ 代码来自己对对象进行排序(有点简单且缩放效果不佳或更好缩放且更复杂),但是有更好的方法吗?

4

10 回答 10

23

并不是 MySQL 和其他数据库自己对事物进行排序,而是它们不对其进行排序。当您调用Model.find([5, 2, 3])时,生成的 SQL 类似于:

SELECT * FROM models WHERE models.id IN (5, 2, 3)

这没有指定顺序,只是您想要返回的记录集。事实证明,通常 MySQL 会按'id'顺序返回数据库行,但不能保证这一点。

让数据库以有保证的顺序返回记录的唯一方法是添加一个 order 子句。如果您的记录将始终以特定顺序返回,那么您可以向数据库添加一个排序列并执行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}} 
于 2009-04-30T07:41:30.013 回答
10

根据我之前对 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]
于 2011-09-13T19:19:19.397 回答
7

显然 mySQL 和其他数据库管理系统会自行排序。我认为您可以绕过这样做:

ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
于 2009-04-29T16:13:19.853 回答
6

一个可移植的解决方案是在您的 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。如果不是这种情况,那么您需要使用ActiveRecord SQL sanitizer 方法connection.quote之一来正确引用您的.ids

然后使用order字符串作为您的订购条件:

Object.find(ids, :order => order)

或在现代世界中:

Object.where(:id => ids).order(order)

这有点冗长,但它应该适用于任何 SQL 数据库,并且隐藏丑陋并不难。

于 2013-03-22T07:08:27.857 回答
4

正如我在这里回答的那样,我刚刚发布了一个 gem ( order_as_specified ),它允许您像这样进行本机 SQL 排序:

Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])

刚刚经过测试,它可以在 SQLite 中运行。

于 2015-03-13T18:30:41.160 回答
3

Justin Weiss就在两天前写了一篇关于这个问题的博客文章。

告诉数据库首选顺序并直接从数据库加载按该顺序排序的所有记录似乎是一种好方法。他的博客文章中的示例:

# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      order_clause = "CASE id "
      ids.each_with_index do |id, index|
        order_clause << "WHEN #{id} THEN #{index} "
      end
      order_clause << "ELSE #{ids.length} END"
      where(id: ids).order(order_clause)
    end
  end
end

ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)

这使您可以编写:

Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
于 2015-04-22T00:57:56.803 回答
2

这是一种高性能(哈希查找,而不是检测中的 O(n) 数组搜索!)单线,作为一种方法:

def find_ordered(model, ids)
  model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end

# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id)          == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
于 2015-10-29T20:40:57.270 回答
1

在 Ruby 中执行此操作的另一种(可能更有效)方法:

ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record| 
  result[record.id] = record
  result
end
sorted_records = ids.map {|id| records_by_id[id] }
于 2011-08-24T11:00:35.157 回答
1

这是我能想到的最简单的事情:

ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
于 2012-06-04T21:08:04.903 回答
0
@things = [5,2,3].map{|id| Object.find(id)}

这可能是最简单的方法,假设您没有太多要查找的对象,因为它需要为每个 id 访问数据库。

于 2009-04-29T16:44:37.423 回答