按ID给出的模型记录按ID数组的顺序查找

Jon*_*han 37 activerecord ruby-on-rails ruby-on-rails-3

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

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

但它们总是以数字顺序提取,我通过执行以下方式知道:

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

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

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

我尝试过类似的东西:

ids.each do |i|
  person = people.where('id = ?', i)
Run Code Online (Sandbox Code Playgroud)

但我认为这不起作用.

Bri*_*ood 30

请注意以下代码:

ids.each do |i|
  person = people.where('id = ?', i)
Run Code Online (Sandbox Code Playgroud)

它有两个问题:

首先,#each方法返回它迭代的数组,所以你只需要返回id.你想要的是收集

其次,where将返回一个Arel :: Relation对象,它最终会被评估为一个数组.所以你最终得到了一个数组数组.你可以修复两种方法.

第一种方式是扁平化:

ids.collect {|i| Person.where('id => ?', i) }.flatten
Run Code Online (Sandbox Code Playgroud)

更好的版本:

ids.collect {|i| Person.where(:id => i) }.flatten
Run Code Online (Sandbox Code Playgroud)

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

ids.collect {|i| Person.find(i) }
Run Code Online (Sandbox Code Playgroud)

这很好,也很简单

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

我喜欢塞尔吉奥的解决方案,但这是我建议的另一个:

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

我发誓,我记得ActiveRecord曾经为我们做过这个ID订购.也许它与Arel一起消失;)


ape*_*ros 11

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

映射ID:

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

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

对结果进行排序:

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] }
Run Code Online (Sandbox Code Playgroud)

或者,扩展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)
Run Code Online (Sandbox Code Playgroud)

希望有所帮助


act*_*ars 10

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

解决方案一:

Person.find([1,2,3,4])
Run Code Online (Sandbox Code Playgroud)

ActiveRecord::RecordNotFound如果没有记录,这将提高

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

Person.find_all_by_id([1,2,3,4])
Run Code Online (Sandbox Code Playgroud)

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

根据您的要求选择您想要在上面使用的方法,然后按给定的方式对它们进行排序 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}}
Run Code Online (Sandbox Code Playgroud)

解决方案[仅限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
Run Code Online (Sandbox Code Playgroud)

  • Rails 4解决方案没有回答这个问题.您按ID的数值排序.这就是Rails默认的方式. (22认同)
  • 问题是如何以传递方式命令ID ...而不是默认的ASC或DESC (8认同)

San*_*ing 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] }


Mas*_*ano 8

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

\n

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

\n

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

\n

什么不起作用

\n
Person.where(id: ids)\n
Run Code Online (Sandbox Code Playgroud)\n

返回的Relation的顺序可以是任意的,也可以是主ID的数值的顺序;不管怎样,它通常与 的观点不一致ids

\n

获取数组的简单解决方案

\n

(仅限 Rails 5+(?))

\n
Person.find ids\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n
Person.find_all_by_id ids\n
Run Code Online (Sandbox Code Playgroud)\n

获取数组的纯 Ruby 解决方案

\n

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

\n
Person.where(id: ids).sort_by{|i| ids.index(i.id)}\nPerson.where(id: ids).index_by(&:id).values_at(*ids)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n

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

\n

对于任何数据库

\n

基于user3033467\'s 答案,但已更新为与 Rails 6 配合使用(出于order()安全考虑,Rails 6 禁用了某些功能;有关背景信息,请参阅 Justin 的“ Rails 6.1 中的 SQL 注入更新”)。

\n
order_query = <<-SQL\n  CASE musics.id \n    #{ids.map.with_index { |id, index| "WHEN #{id} THEN #{index}" } .join(\' \')}\n    ELSE #{ids.length}\n  END\nSQL\n\nPerson.order(Arel.sql(order_query))\n
Run Code Online (Sandbox Code Playgroud)\n

对于 MySQL 特定的

\n

来自Koen的回答(我还没有测试过)。

\n
Person.order(Person.send(:sanitize_sql_array, [\'FIELD(id, ?)\', ids])).find(ids)\n
Run Code Online (Sandbox Code Playgroud)\n

对于 PostgreSQL 特定的

\n

PostgreSQL 9.4+

\n
join_sql = "INNER JOIN unnest(\'{#{ids.join(\',\')}}\'::int[]) WITH ORDINALITY t(id, ord) USING (id)"\nPerson.joins(join_sql).order("t.ord")\n
Run Code Online (Sandbox Code Playgroud)\n

PostgreSQL 8.2+

\n

基于杰夫的回答,但LEFT JOIN替换为INNER JOIN

\n
val_ids = ids.map.with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")\nPerson.joins("INNER JOIN (VALUES #{val_ids}) AS persons_id_order(id, ordering) ON persons.id = persons_id_order.id")\n  .order("persons_id_order.ordering")\n
Run Code Online (Sandbox Code Playgroud)\n

获取较低级别的对象

\n

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

\n

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

\n

获取 ActiveRecord::Result

\n

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

\n

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

\n
str_sql = "select persons.* from persons INNER JOIN unnest(\'{#{ids.join(\',\')}}\'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"\nPerson.connection.select_all(str_sql)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

获取 PG::Result

\n

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

\n
str_sql = "select persons.* from persons INNER JOIN unnest(\'{#{ids.join(\',\')}}\'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"\nPerson.connection.execute(str_sql)\n
Run Code Online (Sandbox Code Playgroud)\n


ell*_*tcm 7

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

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

请注意,使用where而不是find不保留顺序。

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


Ser*_*sev 6

您可以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}]
Run Code Online (Sandbox Code Playgroud)

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

  • 没问题。选择最佳答案:) (2认同)

Koe*_*en. 5

老问题,但排序可以通过使用SQL FIELD函数进行排序来完成.(仅用MySQL测试过.)

所以在这种情况下,这样的事情应该有效:

Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)
Run Code Online (Sandbox Code Playgroud)

这导致以下SQL:

SELECT * FROM people
  WHERE id IN (1, 3, 5, 9, 6, 2)
  ORDER BY FIELD(id, 1, 3, 5, 9, 6, 2)
Run Code Online (Sandbox Code Playgroud)


Jer*_*rph 5

Most of the other solutions don't allow you to further filter the resulting query, which is why I like Koen's answer.

Similar to that answer but for Postgres, I add this function to my ApplicationRecord (Rails 5+) or to any model (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
Run Code Online (Sandbox Code Playgroud)

The query solution is from this question.