清洁方式按指定的顺序通过id查找ActiveRecord对象
我想在给定一组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)
这不指定订单,只指定要返回的记录集。 事实certificate,通常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数据库一样,并且隐藏丑陋并不困难。
正如我在这里回答的那样,我刚刚发布了一个gem( order_as_specified ),允许你像这样进行本机SQL排序:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
刚刚测试过,它可以在SQLite中运行。
就在两天前,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]
这是一个高性能(散列查找,而不是检测中的O(n)数组搜索!)one-liner,作为一种方法:
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
在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] }
这是我能想到的最简单的事情:
ids = [200, 107, 247, 189] results = ModelObject.find(ids).group_by(&:id) sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}
这可能是最简单的方法,假设您没有太多要查找的对象,因为它需要为每个id访问数据库。