我希望在给定一组id的情况下获得一个ActiveRecord对象数组。
我认为
Object.find([5,2,3])
将返回一个数组,其中包含对象5,对象2,然后按顺序返回对象3,但我得到的数组按对象2,对象3和对象5排序。
ActiveRecord Base find method API提到您不应该按照提供的顺序预期它(其他文档没有提供此警告)。
Find by array of ids in the same order?中给出了一个可能的解决方案,但订单选项似乎对SQLite无效。
我可以编写一些ruby代码来自己对对象进行排序(有点简单,缩放比例较差或缩放比较复杂),但是有更好的方法吗?
答案 0 :(得分:22)
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}}
答案 1 :(得分: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]
答案 2 :(得分:6)
显然,mySQL和其他数据库管理系统会自行排序。我认为你可以绕过这样做:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
答案 3 :(得分: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
中的数字或其他安全值;如果情况并非如此,那么您希望使用connection.quote
或其中一个ActiveRecord SQL sanitizer methods正确引用您的ids
。
然后使用order
字符串作为您的订购条件:
Object.find(ids, :order => order)
或在现代世界:
Object.where(:id => ids).order(order)
这有点冗长,但它应该与任何SQL数据库一样,并且隐藏丑陋并不困难。
答案 4 :(得分:4)
当我回答here时,我刚刚发布了一个gem(order_as_specified),它允许你像这样进行本机SQL排序:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
刚刚测试过,它可以在SQLite中运行。
答案 5 :(得分:3)
Justin Weiss两天前写了blog article about this problem。
这似乎是一种很好的方法来告诉数据库有关首选顺序并直接从数据库加载按该顺序排序的所有记录。他的blog article:
示例# 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]
答案 6 :(得分: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
答案 7 :(得分: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] }
答案 8 :(得分:1)
这是我能想到的最简单的事情:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
答案 9 :(得分:-1)
@things = [5,2,3].map{|id| Object.find(id)}
这可能是最简单的方法,假设您没有太多要查找的对象,因为它需要为每个id访问数据库。