【问题标题】:ActiveRecord.find(array_of_ids), preserving orderActiveRecord.find(array_of_ids),保留顺序
【发布时间】:2017-06-18 04:24:38
【问题描述】:

在 Rails 中执行Something.find(array_of_ids) 时,结果数组的顺序不依赖于array_of_ids 的顺序。

有没有办法进行查找并保留订单?

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

UPD:如果可以使用 :order 参数和某种 SQL 子句指定顺序,那么如何?

【问题讨论】:

标签: ruby-on-rails ruby activerecord


【解决方案1】:

find (:order=>'...') 中有一个 order 子句,它在获取记录时执行此操作。 您也可以从这里获得帮助。

link text

【讨论】:

    【解决方案2】:

    在后台,带有 id 数组的 find 将生成带有 WHERE id IN... 子句的 SELECT,这应该比遍历 id 更有效。

    所以请求一趟数据库就满足了,但是没有ORDER BY子句的SELECTs是未排序的。 ActiveRecord 明白这一点,所以我们扩展我们的find 如下:

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

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

    【讨论】:

    • 请注意,选项哈希已被弃用。 (第二个参数,在本例中为:order => id)
    【解决方案3】:

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

    第一个例子:

    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}}
    

    【讨论】:

    • 虽然效率不高,但我同意这种解决方法与 DB 无关,如果行数较少,则可以接受。
    • 不要为此使用注入,这是一张地图:sorted = arr.map { |val| Model.find(val) }
    • 第一个很慢。我同意第二个这样的地图:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
    【解决方案4】:

    答案仅适用于 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 source code

    【讨论】:

    • 你碰巧知道 Postgres 中 FIELDS() 的等价物吗?
    • 我在 postgres 中写了一个 plpgsql 函数来做到这一点 - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
    • 这不再有效。最新的 Rails:Object.where(id: ids).order("field(id, #{ids.join ','})")
    • 这是一个比 Gunchars 更好的解决方案,因为它不会破坏分页。
    • .ids 对我来说很好用,而且速度非常快activerecord documentation
    【解决方案5】:

    奇怪的是,没有人提出这样的建议:

    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 并将哈希返回“重新排序”时,它实际上取决于哈希返回值的添加顺序。这感觉就像取决于可能改变的实现细节。
    • @Jon,Ruby 1.9 和所有其他尝试遵循它的实现都保证了顺序。对于 1.8,Rails (ActiveSupport) 修补了 Hash 类以使其行为方式相同,因此如果您使用 Rails,应该没问题。
    • 感谢您的澄清,刚刚在文档中找到了。
    • 这个问题是它返回一个数组,而不是一个关系。
    • 很好,但是,单线对我不起作用(Rails 4.1)
    【解决方案6】:

    @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)
    

    【讨论】:

      【解决方案7】:

      正如his answer 中的Mike Woodhouse 所述,这是因为Rails 使用带有WHERE id IN... clause 的SQL 查询在一个查询中检索所有记录。这比单独检索每个 id 更快,但正如您所注意到的,它不会保留您正在检索的记录的顺序。

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

      基于Sort an array according to the elements of another array的众多优秀答案,我推荐以下解决方案:

      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)
      

      【讨论】:

      • 第二个解决方案(使用 index_by)对我来说似乎失败了,产生了所有 nil 结果。
      【解决方案8】:

      当我回答 here 时,我刚刚发布了一个 gem (order_as_specified),它允许您像这样进行本机 SQL 排序:

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

      据我测试,它可以在所有 RDBMS 中本地运行,并且返回一个可以链接的 ActiveRecord 关系。

      【讨论】:

      • 老兄,你太棒了。谢谢!
      • 这太好了,谢谢!它是否与当前版本 1.7 中的 rails 6.1 完全兼容?
      【解决方案9】:

      这似乎适用于 postgresql (source) - 并返回 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(Arel.sql(order))
        }    
      end
      
      # usage:
      Something.for_ids_with_order([1, 3, 2])
      

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

      【讨论】:

      • 你是我本周的英雄。谢谢!
      • 请注意,这只适用于微不足道的情况,您最终会遇到您的 ID 包含在列表中的其他 ID 中的情况(例如,它将找到 11 中的 1)。解决此问题的一种方法是将逗号添加到位置检查中,然后将最终逗号添加到连接中,如下所示: order = sanitize_sql_array( ["position(','||clients.id::text||', ' in ?)", ids.join(',') + ','] )
      • 好点,@IrishDubGuy!我会根据你的建议更新我的答案。谢谢!
      • 对我来说,链接不起作用。这里应该在 id:text 之前添加表名:["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] 对我有用的完整版:scope :for_ids_with_order, -&gt;(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id =&gt; ids).order(order) } 感谢@gingerlime @IrishDubGuy
      • 由于弃用错误,我不得不将 order(order) 更改为 order(Arel.sql(order)) 才能使其正常工作。
      【解决方案10】:

      虽然我在 CHANGELOG 的任何地方都没有看到它,但它看起来像这个功能 was changed 随着版本 5.2.0 的发布。

      在这里commit 更新带有5.2.0 标记的文档但是它似乎也已将backported 转换为5.0 版本。

      【讨论】:

        【解决方案11】:

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

        这个概念是利用ORDER BY CASE WHEN SQL 子句。例如:

        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 关系返回(允许您使用 last、count、where、pluck 等方法)

        【讨论】:

          【解决方案12】:

          有一个 gem find_with_order 可以让您使用本机 SQL 查询高效地完成它。

          它同时支持Mysql 和PostgreSQL。

          例如:

          Something.find_with_order(array_of_ids)
          

          如果你想要关系:

          Something.where_with_order(:id, array_of_ids)
          

          【讨论】:

            【解决方案13】:

            参考答案here

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

            【讨论】:

              【解决方案14】:

              根据官方文档,应该是同一个顺序。

              https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find

              Something.find(array_of_ids)

              结果数组的顺序应该与array_of_ids 的顺序相同。我已经在 Rails 6 中对此进行了测试。

              【讨论】:

                【解决方案15】:

                我非常喜欢基于order 的选项。 我的 2c 是把它添加到一个作用域中是有意义的,这样你就可以将它与其他 AR 方法链接起来使用

                scope :find_in_order, ->(ids) { 
                  where(id: ids).order([Arel.sql('FIELD(id, ?)'), ids])
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2022-08-08
                  • 2012-05-31
                  • 2014-01-21
                  • 2014-12-01
                  • 2011-09-06
                  • 1970-01-01
                  相关资源
                  最近更新 更多