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)
如果你有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] }
我在这里总结了解决方案,并添加了最新的 (9.4+) PostgreSQL 特定的解决方案。以下内容基于 Rails 6.1 和 PostgreSQL 12。虽然我提到了针对早期版本的 Rails 和 PostgreSQL 的解决方案,但我还没有使用早期版本实际测试过它们。
\n作为参考,这个问题“ORDER BY the IN value list”给出了使用数据库进行排序/排序的各种方法。
\n在这里,我假设模型保证拥有 ID 数组指定的所有记录ids
。否则,可能会引发类似的异常ActiveRecord::RecordNotFound
(也可能不会,具体取决于方式)。
Person.where(id: ids)\n
Run Code Online (Sandbox Code Playgroud)\n返回的Relation的顺序可以是任意的,也可以是主ID的数值的顺序;不管怎样,它通常与 的观点不一致ids
。
(仅限 Rails 5+(?))
\nPerson.find ids\n
Run Code Online (Sandbox Code Playgroud)\n它按照给定的顺序返回 Person 模型的 Ruby 数组ids
。
缺点是您无法使用 SQL 进一步修改结果。
\n在 Rails 3 中,显然是以下方式,尽管这在其他版本的 Rails 中可能不起作用(当然在 Rails 6 中不起作用)。
\nPerson.find_all_by_id ids\n
Run Code Online (Sandbox Code Playgroud)\n两种方式。无论 Rails 版本如何,两者都可以工作(我认为)。
\nPerson.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
。
以下所有返回Person::ActiveRecord_Relation
,您可以根据需要应用更多过滤器。
在以下解决方案中,将保留所有记录,包括那些 ID 未包含在给定数组中的记录ids
。您可以随时通过添加过滤掉它们where(id: ids)
(这种灵活性是 的优点ActiveRecord_Relation
)。
基于user3033467\'s 答案,但已更新为与 Rails 6 配合使用(出于order()
安全考虑,Rails 6 禁用了某些功能;有关背景信息,请参阅 Justin 的“ Rails 6.1 中的 SQL 注入更新”)。
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来自Koen的回答(我还没有测试过)。
\nPerson.order(Person.send(:sanitize_sql_array, [\'FIELD(id, ?)\', ids])).find(ids)\n
Run Code Online (Sandbox Code Playgroud)\njoin_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基于杰夫的回答,但LEFT JOIN
替换为INNER JOIN
:
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绝大多数情况下,上述解决方案一定优于这些解决方案,但为了完整起见,我放在这里(并在我找到更好的解决方案之前进行记录)\xe2 \x80\xa6
\nids
下面的解决方案中,与上一节介绍的解决方案(可以选择保留所有记录)不同,将过滤掉与in 中的 ID 不匹配的记录。
ActiveRecord::Result
这是PostgreSQL 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;"\nPerson.connection.select_all(str_sql)\n
Run Code Online (Sandbox Code Playgroud)\nPerson.connection.exec_query
返回相同的(别名?)。
PG::Result
这是PostgreSQL 9.4+的解决方案。与上面非常相似,但替换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;"\nPerson.connection.execute(str_sql)\n
Run Code Online (Sandbox Code Playgroud)\n
使用 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)
您可以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的索引。
老问题,但排序可以通过使用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)
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.