将对象数组转换为ActiveRecord :: Relation

129

我有一个对象数组,让我们称之为Indicator。我想在这个数组上运行Indicator类方法(例如def self.subjects、作用域等)。我所知道的唯一在一组对象上运行类方法的方式是将它们作为ActiveRecord::Relation。因此,我最终不得不添加to_indicators方法到Array中。

def to_indicators
  # TODO: Make this less terrible.
  Indicator.where id: self.pluck(:id)
end

有时我会在类方法中链接多个这样的作用域来过滤结果。因此,即使我在ActiveRecord::Relation上调用一个方法,我也不知道如何访问该对象。我只能通过all来获取其内容。但是,all是一个数组。因此,我必须将该数组转换为ActiveRecord::Relation。例如,下面是其中一个方法的一部分:

all.to_indicators.applicable_for_bank(id).each do |indicator|
  total += indicator.residual_risk_for(id)
  indicator_count += 1 if indicator.completed_by?(id)
end

我想这可以归结为两个问题:

  1. 如何将对象数组转换为ActiveRecord::Relation对象?最好不要每次都使用where方法。
  2. 当在ActiveRecord::Relation对象上运行类似def self.subjects的方法时,如何访问该对象本身?

谢谢。如果需要澄清任何事情,请告诉我。


3
如果你试图将数组转换回关系的唯一原因是因为你通过 .all 得到了它,那么就像 Andrew Marshall 的答案所示使用 .scoped(尽管在 Rails 4 中可以使用 .all)。如果你发现自己需要将数组转换为关系,说明你某些地方出错了... - nzifnab
6个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
195
你可以这样将一个对象数组 `arr` 转换为 ActiveRecord::Relation(假设你知道对象所属的类,这很可能是真的)
MyModel.where(id: arr.map(&:id))
你必须使用where,它是一个非常有用的工具,你不应该犹豫地使用它。现在你有了一个一行代码将数组转换为关系。 工作原理如下: map(&:id)会将你的对象数组转换为只包含它们id的数组。将一个数组传递给where子句将生成一个类似于SQL IN语句的查询。
SELECT .... WHERE `my_models`.id IN (2, 3, 4, 6, ....
请记住,数组的顺序将会丢失 - 但由于您的目标只是在这些对象的集合上运行一个类方法,我想这应该不是问题。

3
干得好,正是我所需要的。你可以将其用于模型的任何属性。对我来说完美运作。 - nfriend21
8
为什么要自己编写 SQL 查询语句,当你可以使用 where(id: arr.map(&:id)) 呢?严格来说,这并不会将数组转换为关系,而是在关系实例化后,使用这些 ID 获取对象的新实例,这些实例可能具有与内存中数组中已存在的实例不同的属性值。 - Andrew Marshall
12
这样会丢失顺序。 - Velizar Hristov
12
非常低效!你已经将一个已经存在于内存中的对象集合转化成了需要进行数据库查询才能访问的对象集合。我建议你重构一下那些需要迭代数组的类方法。 - james2m
3
@james2m,你能否想出一种更高效的方式将任何对象数组转换为活动记录关系?据我所知,在Rails中没有内置的方法或方式可以做到这一点。这是在Rails中最实用的方法。 - Paa Yaw
显示剩余3条评论

49
无法将对象数组转换为ActiveRecord :: Relation,因为Relation只是SQL查询的构建器,其方法不对实际数据进行操作。但是,如果您想要关系,则:对于ActiveRecord 3.x,请勿调用all,而是调用scoped,它将返回表示与Array中的所有相同记录的Relation。对于ActiveRecord 4.x,只需调用all即可返回一个Relation。 当在ActiveRecord :: Relation上运行def self.subjects类型的方法时,如何访问ActiveRecord :: Relation对象本身? 当在Relation对象上调用该方法时,self是关系本身(而不是定义在其中的模型类)。

1
请参考下面@Marco Prins提供的解决方案。 - Justin
Rails 5中.push方法被弃用了,有什么替代方法吗? - Jaswinder
@GstjiSaini 我不确定你所指的确切方法,请提供文档或源链接。但是,如果它已被弃用,那么这不是一个很可行的解决方案,因为它很可能很快就会消失。 - Andrew Marshall
这就是为什么你可以使用 class User; def self.somewhere; where(location: "somewhere"); end; end,然后 User.limit(5).somewhere - Dorian
这是唯一真正启发OP的答案。该问题揭示了对ActiveRecord工作原理的错误了解。@Justin只是强调了为什么不应该继续传递对象数组以便映射并构建另一个不必要的查询的缺乏理解。 - james2m

10

在我的情况下,我需要将一个对象数组转换为ActiveRecord::Relation,并使用特定的列(id)进行排序。由于我正在使用MySQL,因此field函数可能会有所帮助。

MyModel.where('id in (?)',ids).order("field(id,#{ids.join(",")})") 

SQL看起来像:

SELECT ... FROM ... WHERE (id in (11,5,6,7,8,9,10))  
ORDER BY field(id,11,5,6,7,8,9,10)

MySQL field函数


如果需要与PostgreSQL兼容的版本,请参考以下帖子:https://dev59.com/xHM_5IYBdhLWcg3wmkUK - armchairdj
ActiveRecord::StatementInvalid(PG :: TooManyArguments:ERROR:无法将超过100个参数传递给函数)这种解决方法不能处理超过100个参数的函数,因此,它适用于总长度长达100的关系,像112和12这样的ID不会相互分离。 - Anton Semenichenko
那么 MyModel.where(id: ids) 呢? - coisnepe

4

首先,这并不是一种万能的解决方案。根据我的经验,与其他方法相比,转换为关系有时更容易。我尽量保持谨慎,并且只在替代方法更加复杂的情况下使用这种方法。

话虽如此,这就是我的解决方案,我已扩展了Array类。

# lib/core_ext/array.rb

class Array

  def to_activerecord_relation
    return ApplicationRecord.none if self.empty?

    clazzes = self.collect(&:class).uniq
    raise 'Array cannot be converted to ActiveRecord::Relation since it does not have same elements' if clazzes.size > 1

    clazz = clazzes.first
    raise 'Element class is not ApplicationRecord and as such cannot be converted' unless clazz.ancestors.include? ApplicationRecord

    clazz.where(id: self.collect(&:id))
  end
end
一个用例是:array.to_activerecord_relation.update_all(status: 'finished')。现在我该在哪里使用它呢? 有时候你需要过滤掉ActiveRecord::Relation,例如取出未完成的元素。在这种情况下,最好使用elements.not_finished作为范围,这样你还可以保留ActiveRecord::Relation。 但有时候条件更加复杂。比如,取出所有未完成的元素,这些元素在最近4周内生成并进行了检查。为了避免创建新的范围,您可以将其过滤到一个数组中,然后再转换回来。请记住,您仍然会查询数据库,因为它按id搜索,但是查询速度很快。

1

ActiveRecord::Relation绑定了从数据库检索数据的数据库查询。

假设我们有一个由相同类的对象组成的数组,那么我们应该使用哪个查询来绑定它们?

当我运行时,

users = User.where(id: [1,3,4,5])
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc
在上面的代码中,users返回一个Relation对象,但它背后绑定了数据库查询,并且您可以查看它。
users.to_sql
 => "SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc"
因此,无法从独立于 SQL 查询的对象数组中返回 ActiveRecord::Relation

0
在PostgreSQL中,为了维护顺序(基于this answer),给定对象数组arr:
ids = arr.pluck(:id)
cases = ids.map.with_index { |id, index| "WHEN id='#{id}' THEN #{index + 1}" }.join(" ")
Model.where(id: ids).order(Arel.sql("CASE #{cases} ELSE #{ids.size + 1} END"))

生成的示例SQL:

SELECT "records".*
FROM "records"
WHERE "records"."id"
IN (
  '84b61039-193b-4263-9de6-5185e2cf4e06',
  '5955c8d0-a809-4e13-b15a-ac3a1b17569e',
  '37c44652-3195-4839-8130-409bb4c3a564'
  )
ORDER BY
  CASE
  WHEN id='84b61039-193b-4263-9de6-5185e2cf4e06' THEN 1
  WHEN id='5955c8d0-a809-4e13-b15a-ac3a1b17569e' THEN 2
  WHEN id='37c44652-3195-4839-8130-409bb4c3a564' THEN 3
  ELSE 4
END;

注意:它不能与.distinct一起使用;在这种情况下,请使用ids = arr.pluck(:id).uniq!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,