find_each 相当于 foo_ids?

Equivalent of find_each for foo_ids?

鉴于此模型:

class User < ActiveRecord::Base
  has_many :things
end

然后我们可以这样做::

@user = User.find(123)
@user.things.find_each{ |t| print t.name }
@user.thing_ids.each{ |id| print id }

有大量 @user.things,我只想批量遍历它们的 ID,就像 find_each 一样。有没有方便的方法来做到这一点?

目标是:

你可以试试下面的方法,每个切片一次取 4 个元素,你可以循环 4 个元素

@user.thing_ids.each_slice(4) do |batch|
  batch.each do |id|
   puts id
   end
end

更新最终编辑:

在查看了您更新的问题后,我更新了我的答案(不确定为什么在我用源代码备份我的答案以证明这一点后您会投反对票...但我不怀恨在心:)

这是我的解决方案,经过测试和工作,如果您满意,可以接受它作为答案。

下面,我扩展了 ActiveRecord::Relation,覆盖了 find_in_batches 方法以接受一个额外的选项,:relation。当设置为 true 时,它​​将 return 与您的块的 activerecord 关系,因此您可以使用所需的方法 'pluck' 仅获取目标查询的 ID。

#put this file in your lib directory:
#active_record_extension.rb
module ARAExtension
  extend ActiveSupport::Concern

  def find_in_batches(options = {})
    options.assert_valid_keys(:start, :batch_size, :relation)

    relation = self
    start = options[:start]
    batch_size = options[:batch_size] || 1000

    unless block_given?
      return to_enum(:find_in_batches, options) do
        total = start ? where(table[primary_key].gteq(start)).size : size
        (total - 1).div(batch_size) + 1
      end
    end

    if logger && (arel.orders.present? || arel.taken.present?)
      logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
    end

    relation = relation.reorder(batch_order).limit(batch_size)
    records = start ? relation.where(table[primary_key].gteq(start)) : relation

    records = records.to_a unless options[:relation]

    while records.any?
      records_size = records.size
      primary_key_offset = records.last.id
      raise "Primary key not included in the custom select clause" unless primary_key_offset

      yield records

      break if records_size < batch_size

      records = relation.where(table[primary_key].gt(primary_key_offset))
      records = records.to_a unless options[:relation]
    end
  end

end

ActiveRecord::Relation.send(:include, ARAExtension)

这里是初始化器

#put this file in config/initializers directory:
#extensions.rb
require "active_record_extension"

最初,此方法强制将关系转换为 activrecord 对象数组,并 returned 给您。现在,我可以选择允许您在转换为数组之前 return 查询。这是一个如何使用它的例子:

@user.things.find_in_batches(:batch_size=>10, :relation=>true).each do |batch_query|
  # do any kind of further querying/filtering/mapping that you want

  # show that this is actually an activerecord relation, not an array of AR objects
  puts batch_query.to_sql
  # add more conditions to this query, this is just an example
  batch_query = batch_query.where(:color=>"blue")
  # pluck just the ids
  puts batch_query.pluck(:id)
end

最终,如果您不喜欢 SO post 上给出的任何答案,您可以推出自己的解决方案。只有当答案偏离主题或没有任何帮助时,才考虑投反对票。我们都只是想提供帮助。否决具有源代码的答案只会阻止其他人尝试帮助您。

上次编辑

回应您的评论(因为我的评论不合适):

  1. 打电话 thing_ids 内部使用 pluck
  2. pluck 内部使用 select_all
  3. ...实例化一个 activerecord Result

之前的第二次编辑:

这行代码在 pluck returns 一个 activerecord Result:

 ....
 result = klass.connection.select_all(relation.arel, nil, bound_attributes)
 ...

我刚刚为您逐步浏览了源代码。使用 select_all 会节省一些内存,但最终,即使您使用 pluck 方法,仍会创建并映射一个 activerecord Result

我会用这样的东西:

User.things.find_each(batch_size: 1000).map(&:id)

这将为您提供一组 ID。

不幸的是,它不是一个单一的班轮或帮手,可以让你做到这一点,所以改为:

limit = 1000
offset = 0
loop do
  batch = @user.things.limit(limit).offset(offset).pluck(:id)
  batch.each { |id| puts id }
  break if batch.count < limit
  offset += limit
end

Rails 5 引入了关系的in_batches method, which yields a relation and uses pluck(primary_key) internally. And we can make use of the where_values_hash方法以检索已经采摘的ids:

@user.things.in_batches { |batch_rel| p batch_rel.where_values_hash['id'] }

请注意,in_batches 具有类似于 find_eachorderlimit 限制。

这种方法有点老套,因为它依赖于 in_batches 的内部实现,如果 in_batches 将来停止提取 ID,将会失败。一个非 hacky 方法是 batch_rel.pluck(:id),但这会运行相同的 pluck 查询两次。