【问题标题】:Find model records by ID in the order the array of IDs were given按照给定 ID 数组的顺序按 ID 查找模型记录
【发布时间】:2012-04-26 08:49:43
【问题描述】:

我有一个查询要按特定顺序获取人员的 ID,例如: ids = [1, 3, 5, 9, 6, 2]

然后我想通过Person.find(ids) 获取这些人

但它们总是按数字顺序获取,我通过执行知道这一点:

people = Person.find(ids).map(&:id)
 => [1, 2, 3, 5, 6, 9]

如何运行此查询以使顺序与 ids 数组的顺序相同?

我使这项任务变得更加困难,因为我只想从给定的 ID 中执行一次获取人员的查询。因此,执行多个查询是不可能的。

我尝试了类似的方法:

ids.each do |i|
  person = people.where('id = ?', i)

但我认为这行不通。

【问题讨论】:

    标签: ruby-on-rails ruby-on-rails-3 activerecord


    【解决方案1】:

    我在这里总结了解决方案,并添加了最近(9.4+)PostgreSQL 特定的解决方案。以下是基于 Rails 6.1 和 PostgreSQL 12 的。虽然我提到了针对早期版本的 Rails 和 PostgreSQL 的解决方案,但我并没有实际测试过早期版本。

    作为参考,question "ORDER BY the IN value list" 给出了使用数据库进行排序/排序的各种方法。

    在这里,我假设模型保证具有 ID 数组 ids 指定的所有记录。否则,可能会引发像 ActiveRecord::RecordNotFound 这样的异常(也可能不会,取决于方式)。

    什么不起作用

    Person.where(id: ids)
    

    返回的 Relation 的顺序可以是任意的,也可以是主 ID 的数值顺序;无论哪种情况,它通常与ids的不一致。

    获取数组的简单解决方案

    (仅限 Rails 5+(?))

    Person.find ids
    

    它按照给定的ids 的顺序返回一个 Ruby 人物模型数组。

    缺点是您无法使用 SQL 进一步修改结果。

    在 Rails 3 中,显然是以下方式,尽管在其他版本的 Rails 中这可能不起作用(当然在 Rails 6 中不起作用)。

    Person.find_all_by_id ids
    

    获取数组的纯 Ruby 解决方案

    两种方式。无论 Rails 版本如何(我认为),两者都可以工作。

    Person.where(id: ids).sort_by{|i| ids.index(i.id)}
    Person.where(id: ids).index_by(&:id).values_at(*ids)
    

    它按照给定的ids 的顺序返回一个 Ruby 的 Person 模型数组。

    获取关系的数据库级解决方案

    以下所有返回Person::ActiveRecord_Relation,如果您愿意,可以对其应用更多过滤器。

    在以下解决方案中,所有记录都被保留,包括那些 ID 未包含在给定数组 ids 中的记录。您可以随时通过添加where(id: ids) 将它们过滤掉(这种灵活性是ActiveRecord_Relation 的优点)。

    适用于任何数据库

    基于 user3033467's answer,但已更新为可与 Rails 6 一起使用(出于安全考虑,Rails 6 已禁用 order() 的某些功能;有关背景信息,请参阅 Justin 的“Updates for SQL Injection in Rails 6.1”)。

    order_query = <<-SQL
      CASE musics.id 
        #{ids.map.with_index { |id, index| "WHEN #{id} THEN #{index}" } .join(' ')}
        ELSE #{ids.length}
      END
    SQL
    
    Person.order(Arel.sql(order_query))
    

    对于 MySQL 特定

    来自Koen's answer(我没有测试过)。

    Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)
    

    针对 PostgreSQL 特定

    PostgreSQL 9.4+

    join_sql = "INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id)"
    Person.joins(join_sql).order("t.ord")
    

    PostgreSQL 8.2+

    基于Jerph's answer,但LEFT JOIN替换为INNER JOIN

    val_ids = ids.map.with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")
    Person.joins("INNER JOIN (VALUES #{val_ids}) AS persons_id_order(id, ordering) ON persons.id = persons_id_order.id")
      .order("persons_id_order.ordering")
    

    获取低级对象

    以下是获取较低级别对象的解决方案。 在绝大多数情况下,上述解决方案必须优于这些解决方案,但为了完整起见我放在这里(并在我找到更好的解决方案之前记录)......

    在下面的解决方案中,与ids中的ID不匹配的记录将被过滤掉,这与上一节中描述的解决方案不同(可以选择保留所有记录)。

    获取 ActiveRecord::Result

    这是使用 PostgreSQL 9.4+ 获取 ActiveRecord::Result 的解决方案。

    ActiveRecord::Result 类似于哈希数组。

    str_sql = "select persons.* from persons INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"
    Person.connection.select_all(str_sql)
    

    Person.connection.exec_query 返回相同(别名?)。

    获取 PG::Result

    这是使用 PostgreSQL 9.4+ 获取 PG::Result 的解决方案。与上面非常相似,但将exec_query 替换为execute(第一行与上面的解决方案相同):

    str_sql = "select persons.* from persons INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"
    Person.connection.execute(str_sql)
    

    【讨论】:

      【解决方案2】:

      我尝试了在 Rails6 上推荐 FIELD 方法的答案,但遇到了错误。但是,我发现只需将 sql 包装在 Arel.sql() 中即可。

      # Make sure it's a known-safe values.
      user_ids = [3, 2, 1]
      # Before 
      users = User.where(id: user_ids).order("FIELD(id, 2, 3, 1)")
      # With warning.
      
      # After 
      users = User.where(id: user_ids).order(Arel.sql("FIELD(id, 2, 3, 1)"))
      # No warning
      

      [1]https://medium.com/@mitsun.chieh/activerecord-relation-with-raw-sql-argument-returns-a-warning-exception-raising-8999f1b9898a

      【讨论】:

        【解决方案3】:

        这在 SQL 中通过 ActiveRecord 而不是在 Ruby 中最有效地处理。

        ids = [3,1,6,7,12,2]
        Post.where(id: ids).order("FIELD(id, #{ids.join(',')})")
        

        【讨论】:

          【解决方案4】:

          要按特定顺序获取人员的 ID,请说:ids = [1, 3, 5, 9, 6, 2]

          在旧版本的rails中,find和where按数字顺序获取数据,但rails 5以与查询相同的顺序获取数据

          注意:查找保留顺序和不保留的地方

          Person.find(ids).map(&:id)
          => [1, 3, 5, 9, 6, 2]
          
          Person.where(id: ids).map(&:id)
          => [1, 2, 3, 5, 6, 9] 
          

          但它们总是按数字顺序获取,我通过执行知道这一点:

          【讨论】:

            【解决方案5】:

            这个简单的解决方案成本低于加入值:

            order_query = <<-SQL
              CASE persons.id 
                #{ids.map.with_index { |id, index| "WHEN #{id} THEN #{index}" } .join(' ')}
                ELSE #{ids.length}
              END
            SQL
            Person.where(id: ids).order(order_query)
            

            【讨论】:

            • +1 为此应该可能工作,无论数据库如何。但我发现 Arel() 的修改对于 Rails 6.1 是必不可少的 order(Arel.sql(order_query))
            【解决方案6】:

            在 Rails 5 中,我发现这种方法有效(至少适用于 postgres),即使对于范围查询,也适用于使用 ElasticSearch:

            Person.where(country: "France").find([3, 2, 1]).map(&:id)
            => [3, 2, 1]
            

            请注意,使用where 代替find 不会保留顺序。

            Person.where(country: "France").where(id: [3, 2, 1]).map(&:id)
            => [1, 2, 3]
            

            【讨论】:

            • 使用 find 而不是在哪里有诀窍。非常感谢!
            • 这可行,但是如果传递的 id 不存在,它将抛出异常
            【解决方案7】:

            大多数其他解决方案不允许您进一步过滤结果查询,这就是我喜欢Koen's answer 的原因。

            与该答案类似,但对于 Postgres,我将此函数添加到我的 ApplicationRecord(Rails 5+)或任何模型(Rails 4):

            def self.order_by_id_list(id_list)
              values_clause = id_list.each_with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")
              joins("LEFT JOIN (VALUES #{ values_clause }) AS #{ self.table_name}_id_order(id, ordering) ON #{ self.table_name }.id = #{ self.table_name }_id_order.id")
                .order("#{ self.table_name }_id_order.ordering")
            end
            

            查询解决方案来自this question

            【讨论】:

            • 很好的答案!这正是我想要的,谢谢! :)
            • 您需要将 LEFT JOIN 替换为 JOIN 以匹配原始查询,以便此方法按预期工作。
            【解决方案8】:

            老问题,但是可以使用 SQL FIELD 函数进行排序。 (仅用 MySQL 测试过。)

            所以在这种情况下应该可以这样:

            Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)
            

            这会导致以下 SQL:

            SELECT * FROM people
              WHERE id IN (1, 3, 5, 9, 6, 2)
              ORDER BY FIELD(id, 1, 3, 5, 9, 6, 2)
            

            【讨论】:

              【解决方案9】:

              如果你有 ids 数组,那么它就像 - Person.where(id: ids).sort_by {|p| ids.index(p.id) } 或者

              persons = Hash[ Person.where(id: ids).map {|p| [p.id, p] }] ids.map {|i| persons[i] }

              【讨论】:

                【解决方案10】:

                有两种方法可以通过给定的 id 数组获取条目。如果您正在使用 Rails 4,不推荐使用动态方法,您需要查看下面的 Rails 4 特定解决方案。

                解决方案一:

                Person.find([1,2,3,4])
                

                如果不存在记录,这将引发ActiveRecord::RecordNotFound

                解决方案二 [仅限 Rails 3]:

                Person.find_all_by_id([1,2,3,4])
                

                这不会导致异常,如果没有记录匹配您的查询,只需返回空数组。

                根据您的要求在上面选择您想使用的方法,然后按照给定的ids 对它们进行排序

                ids = [1,2,3,4]
                people = Person.find_all_by_id(ids)
                # alternatively: people = Person.find(ids)
                ordered_people = ids.collect {|id| people.detect {|x| x.id == id}}
                

                解决方案 [仅限 Rails 4]:

                我认为 Rails 4 提供了更好的解决方案。

                # without eager loading
                Person.where(id: [1,2,3,4]).order('id DESC')
                
                # with eager loading.
                # Note that you can not call deprecated `all`
                Person.where(id: [1,2,3,4]).order('id DESC').load
                

                【讨论】:

                • Rails 4 解决方案没有回答这个问题。您按 id 的数值排序。默认情况下,Rails 就是这样做的。
                • 问题询问如何以传入的方式对 id 进行排序......而不是默认的 ASC 或 DESC
                【解决方案11】:

                您可以从数据库中获取按id asc 排序的用户,然后在应用程序中以您想要的任何方式重新排列它们。看看这个:

                ids = [1, 3, 5, 9, 6, 2]
                users = ids.sort.map {|i| {id: i}} # Or User.find(ids) or another query
                
                # users sorted by id asc (from the query)
                users # => [{:id=>1}, {:id=>2}, {:id=>3}, {:id=>5}, {:id=>6}, {:id=>9}]
                
                users.sort_by! {|u| ids.index u[:id]} 
                
                # users sorted as you wanted
                users # => [{:id=>1}, {:id=>3}, {:id=>5}, {:id=>9}, {:id=>6}, {:id=>2}]
                

                这里的技巧是通过一个人工值对数组进行排序:另一个数组中对象 id 的索引。

                【讨论】:

                • 刚刚睡了,早上试试这个,谢谢!
                • 能否请您详细说明您的回答?我可以看到它有效,但不确定如何将它与人、姓名、电子邮件等一起使用。
                • 是的,当然。关键行是带有sort_by! 的行。您提供一个块,然后使用用户实例调用该块。你从这个对象中提取一个 id,在你的原始 id 数组中找到这个 id 的位置,这个位置也将是用户对象在排序数组中的位置。 id=5 的用户将排在第三位,因为 ids 数组在第三位有 3。
                • 那么在什么时候你会做类似users = User.find_all_by_id(ids) 的事情来让实际的集合进行排序?
                • 没问题。选择最佳答案:)
                【解决方案12】:

                在我看来,您可以映射 ID 或对结果进行排序。对于后者,已经有解决方案,尽管我发现它们效率低下。

                映射 ID:

                ids = [1, 3, 5, 9, 6, 2]
                people_in_order = ids.map { |id| Person.find(id) }
                

                请注意,这将导致执行多个查询,这可能是低效的。

                排序结果:

                ids = [1, 3, 5, 9, 6, 2]
                id_indices = Hash[ids.map.with_index { |id,idx| [id,idx] }] # requires ruby 1.8.7+
                people_in_order = Person.find(ids).sort_by { |person| id_indices[person.id] }
                

                或者,扩展 Brian Underwoods 的答案:

                ids = [1, 3, 5, 9, 6, 2]
                indexed_people = Person.find(ids).index_by(&:id) # I didn't know this method, TIL :)
                people_in_order = indexed_people.values_at(*ids)
                

                希望有帮助

                【讨论】:

                • 谢谢您,我尝试了您的扩展答案,哪个有效 - 这是您的首选解决方案吗?
                • 我还没有使用 index_by,但假设它没有带来一些莫名其妙的高性能损失,那么是的。我认为它非常简洁。
                • 实际上它似乎是这里更快的解决方案之一 - 但每个解决方案的时间都非常接近,虽然我没有测试你的第一个解决方案 - 我必须相信你的话是否你觉得这样会更有效率
                • 正如我在答案中所说,由于使用多个查询的开销,第一个解决方案可能非常慢。对于少数 id,这无关紧要。但我更喜欢第三个。
                • 最后一个替代方案对我有用 - 使用 Mongoid。
                【解决方案13】:

                关于此代码的注释:

                ids.each do |i|
                  person = people.where('id = ?', i)
                

                它有两个问题:

                首先,#each 方法返回迭代的数组,因此您只需返回 id。你想要的是收藏

                其次,where 将返回一个 Arel::Relation 对象,该对象最终将评估为一个数组。所以你最终会得到一个数组数组。你可以解决两种方法。

                第一种方法是展平:

                ids.collect {|i| Person.where('id => ?', i) }.flatten
                

                更好的版本:

                ids.collect {|i| Person.where(:id => i) }.flatten
                

                第二种方法是简单地进行查找:

                ids.collect {|i| Person.find(i) }
                

                很好很简单

                但是,您会发现,这些都对每次迭代进行查询,因此效率不高。

                我喜欢 Sergio 的解决方案,但我会提出另一个建议:

                people_by_id = Person.find(ids).index_by(&:id) # Gives you a hash indexed by ID
                ids.collect {|id| people_by_id[id] }
                

                我发誓我记得 ActiveRecord 曾经为我们执行此 ID 排序。也许它与 Arel 一起消失了;)

                【讨论】:

                • 刚刚在我拥有的一个旧的 2.3.8 项目中尝试过,结果发现 find 方法不会自动按 ID 排序。我一定老了……;)
                • 您能稍微解释一下您更改后的建议吗?然后在什么时候可以使用它来遍历新人员以产生正确的顺序?还是因为它不起作用而发表评论?谢谢你的意见,我发现它非常有用
                • 我已经尝试过您的解决方案,它的基准测试速度比这里的任何其他解决方案都要快,谢谢!
                • 我可能应该更清楚,您可以使用第二行并对其进行分配或进一步迭代。我也可能应该从我的替代解决方案开始,尽管我喜欢从一些代码开始并显示增量更改,直到达到更理想的解决方案;)
                • index_by 很棒,但请注意您是否收到由 HTTP 填充的 id 数组。花了近一个小时才意识到 ["1", "2", "3"] != [1, 2, 3]。
                猜你喜欢
                • 2016-09-03
                • 1970-01-01
                • 2018-07-27
                • 1970-01-01
                • 2015-01-30
                • 2014-08-01
                • 1970-01-01
                • 2010-10-22
                相关资源
                最近更新 更多