ActiveRecord.find(array_of_ids),保留顺序


98

当你Something.find(array_of_ids)在Rails中,所得到的数组的顺序不依赖的顺序array_of_ids

有什么方法可以查找和保留订单吗?

我通过ATM手动根据ID的顺序对记录进行排序,但这有点la脚。

UPD:如果可以使用:orderparam和某种SQL子句指定顺序,那怎么办?


Answers:


71

答案仅适用于mysql

mysql中有一个名为FIELD()的函数

这是在.find()中使用它的方式:

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

更新:这将在Rails 6.1 Rails源代码中删除


您是否碰巧知道FIELDS()Postgres中的?
TrungLê12年

3
我在postgres中编写了一个plpgsql函数来执行此操作-omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi

25
这不再起作用。欲了解更多最新的Rails:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff,2015年

2
这是比Gunchars更好的解决方案,因为它不会破坏分页。
pguardiario

.ids对我来说很好用,并且是非常有效的activerecord文档
TheJKFever 2015年

79

奇怪的是,没有人建议过这样的事情:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

除了让SQL后端执行此操作外,它的效率也很高。

编辑:为了改善我自己的答案,您也可以这样做:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_by并且#slice分别在ActiveSupport中非常方便地添加了数组和哈希。


因此,您的编辑似乎有效,但无法保证哈希中的紧张键顺序,对吗?因此,当您调用slice并将哈希返回“重新排序”时,它实际上取决于哈希返回值的添加顺序(键的添加顺序)。感觉好像取决于可能更改的实现细节。
2013年

2
@Jon,在Ruby 1.9和其他所有尝试遵循它的实现中都保证了该顺序。对于1.8,Rails(ActiveSupport)对Hash类进行修补以使其具有相同的行为,因此,如果您使用Rails,应该可以。
Gunchars

感谢您的澄清,只是在文档中找到了。
2013年

13
问题是它返回一个数组,而不是一个关系。
Velizar Hristov 2015年

3
太好了,但是,
单线

44

正如Mike Woodhouse回答中所说的那样,这可能是由于Rails在后台使用了带a的SQL查询WHERE id IN... clause来检索一个查询中的所有记录。这比分别检索每个id更快,但是正如您注意到的那样,它并不能保留您要检索的记录的顺序。

为了解决此问题,您可以根据在查找记录时使用的ID的原始列表在应用程序级别对记录进行排序。

基于对根据另一个数组的元素对数组进行排序的许多出色答案,我建议以下解决方案:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

或者,如果您需要更快的速度(但可能可读性较低),可以这样做:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)

3
第二个解决方案(使用index_by)对我来说似乎失败,产生所有nil结果。
本·惠勒

22

这似乎适用于postgresqlsource)-并返回ActiveRecord关系

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(order)
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

也可以扩展到其他列,例如,对于name列,使用position(name::text in ?)...


你是我本周的英雄。谢谢!
ntdb '16

4
请注意,这仅在平凡的情况下有效,您最终会遇到您的ID包含在列表中其他ID内的情况(例如,它将在11中找到1)。解决此问题的一种方法是将逗号添加到位置检查中,然后将最终逗号添加到联接中,如下所示:order = sanitize_sql_array([“ position(','|| clients.id :: text ||', 'in?)“,ids.join(',')+','])
IrishDubGuy

好点,@ IrishDubGuy!我将根据您的建议更新答案。谢谢!
gingerlime

对我来说,链接不起作用。这里的表名应该在id:text之前添加:["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] 对我scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } 有用的完整版本:谢谢@gingerlime @IrishDubGuy
user1136228

我猜您需要添加表名,以防万一您进行了一些连接……这在您加入ActiveRecord范围时很常见。
生姜

19

正如我在这里回答的那样,我刚刚发布了一个gem(order_as_specified),它使您可以执行本机SQL排序,如下所示:

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

据我所能进行的测试,它在所有RDBMS中都可以正常运行,并且它返回可以链接的ActiveRecord关系。


1
杜德,你真棒。谢谢!
swrobel '16

5

不幸的是,在不可能在所有情况下都能正常工作的SQL中,您可能需要为红宝石的每条记录或订单编写单个查找,尽管可能有一种使用专有技术使其工作的方法:

第一个例子:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

效率很低

第二个例子:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}

尽管效率不是很高,但我同意这种解决方法与数据库无关,如果您的行数少,则可以接受。
TrungLê12年

不要为此使用注入功能,它是一张地图:sorted = arr.map { |val| Model.find(val) }
tokland

第一个很慢。我同意第二张地图,如下所示:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon 2014年

2

@Gunchars的答案很好,但是在Rails 2.3中它不是开箱即用的,因为Hash类没有排序。一个简单的解决方法是扩展Enumerable类index_by以使用OrderedHash类:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

现在,@ Gunchars的方法将起作用

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

奖金

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

然后

Something.find_with_relevance(array_of_ids)

2

假设Model.pluck(:id)回报,[1,2,3,4]并且您想要的顺序[2,4,1,3]

概念是利用ORDER BY CASE WHENSQL子句。例如:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

在Rails中,可以通过在模型中使用公共方法来构造类似的结构来实现此目的:

def self.order_by_ids(ids)
  if ids.present?
    order_by = ["CASE"]
    ids.each_with_index do |id, index|
      order_by << "WHEN id='#{id}' THEN #{index}"
    end
    order_by << "END"
    order(order_by.join(" "))
  end
else
  all # If no ids, just return all
end

然后做:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

关于这个的好事。结果表明ActiveRecord的关系返回(允许你使用类似的方法lastcountwherepluck,等)


2

有一个宝石find_with_order,它允许您使用本机SQL查询来高效地执行此操作。

它同时支持MysqlPostgreSQL

例如:

Something.find_with_order(array_of_ids)

如果您想建立关系:

Something.where_with_order(:id, array_of_ids)

1

在幕后,find使用ID数组将生成SELECT带有WHERE id IN...子句的,它比遍历ID的效率更高。

因此,一次访问数据库就满足了该请求,但是SELECT不带ORDER BY子句的s 未排序。ActiveRecord理解这一点,因此我们将其扩展find如下:

Something.find(array_of_ids, :order => 'id')

如果数组中id的顺序是任意且重要的(即,无论数组中包含的id顺序如何,您都希望返回的行顺序与数组匹配),那么我认为通过对结果进行后处理,您将是最好的服务器代码-您可以构建一个:order子句,但它会非常复杂,而且根本不涉及意图。


请注意,选项哈希已被弃用。(在此示例中为第二个参数:order => id
ocodo 2012年

1

尽管我没有在CHANGELOG中看到它,但该功能似乎随着版本的发布而改变5.2.0

在这里提交更新带有标记的文档,5.2.0但是它似乎也已经反向移植到了version中5.0


0

参考这里的答案

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") 适用于Postgresql。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.