47

我有一个查询要按特定顺序获取人员的 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)

但我认为这行不通。

4

13 回答 13

35

请注意此代码:

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 一起消失了;)

于 2012-04-14T01:37:10.773 回答
16

正如我所看到的,您可以映射 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)

希望有帮助

于 2012-04-14T03:13:46.987 回答
13

如果你有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] }

于 2014-09-04T11:00:40.137 回答
9

有两种方法可以通过给定的 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
于 2012-04-14T02:13:41.970 回答
8

您可以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 的索引。

于 2012-04-14T01:23:35.957 回答
8

使用 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]
于 2018-12-29T21:03:17.513 回答
6

FIELD老问题,但是可以通过使用 SQL函数进行排序来完成排序。(仅用 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)
于 2016-07-14T15:35:32.883 回答
5

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

与该答案类似,但对于 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

于 2017-04-05T16:13:21.790 回答
2

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

ids = [3,1,6,7,12,2]
Post.where(id: ids).order("FIELD(id, #{ids.join(',')})")
于 2017-06-18T17:51:45.453 回答
2

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

作为参考,这个问题“ORDER BY the IN value list”给出了使用数据库进行排序/排序的各种方式。

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

什么不起作用

Person.where(id: ids)

返回的 Relation 的顺序可以是任意的,也可以是主 ID 的数值顺序;无论如何,它通常不同意 的ids

获取数组的简单解决方案

(仅限 Rails 5+(?))

Person.find ids

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

缺点是您无法使用 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)

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

获取 Relation 的 DB 级解决方案

以下所有返回Person::ActiveRecord_Relation,您可以根据需要对其应用更多过滤器。

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

对于任何数据库

基于user3033467 的回答,但已更新为可与 Rails 6 一起使用(order()出于安全考虑,它已禁用某些功能;请参阅Justin 的“ Rails 6.1 中的 SQL 注入更新”作为背景)。

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的回答(我没有测试过)。

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 的回答,但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")

获取较低级别的对象

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

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

获取 ActiveRecord::Result

这是ActiveRecord::ResultPostgreSQL 9.4+ 的解决方案。

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

这是PG::ResultPostgreSQL 9.4+ 的解决方案。与上面非常相似,但替换exec_queryexecute(第一行与上面的解决方案相同):

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)
于 2021-08-31T12:11:48.657 回答
0

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

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)
于 2020-04-17T08:41:41.837 回答
0

要按特定顺序获取人员的 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] 

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

于 2020-06-30T06:03:57.840 回答
0

我尝试了在 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

于 2021-03-24T10:47:52.980 回答