跳至内容 跳至搜索

Active Record 批量操作

命名空间
方法
F
I

常量

DEFAULT_ORDER = :asc
 
ORDER_IGNORE_MESSAGE = "作用域排序被忽略,请使用 :cursor 和 :order 配置自定义排序。"
 

实例公有方法

find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, &block)

循环遍历数据库中的一组记录(例如,使用 Scoping::Named::ClassMethods.all 方法)效率非常低,因为它会尝试一次性实例化所有对象。

在这种情况下,批量处理方法允许您以批次的方式处理记录,从而极大地减少内存消耗。

The find_each 方法使用 find_in_batches,批次大小为 1000(或由 :batch_size 选项指定)。

Person.find_each do |person|
  person.do_awesome_stuff
end

Person.where("age > 21").find_each do |person|
  person.party_all_night!
end

如果您不为 find_each 提供块,它将返回一个枚举器,用于与其他方法链式调用。

Person.find_each.with_index do |person, index|
  person.award_trophy(index + 1)
end

选项

  • :batch_size - 指定批次的大小。默认为 1000。

  • :start - 指定要开始的游标列值,包括该值。

  • :finish - 指定要结束的游标列值,包括该值。

  • :error_on_ignore - 覆盖应用程序配置以指定在关联中存在排序时是否应引发错误。

  • :cursor - 指定用于批处理的列(可以是列名或列名数组)。默认为主键。

  • :order - 指定游标列的顺序(可以是 :asc:desc 或包含 :asc 或 :desc 的数组)。默认为 :asc

    class Order < ActiveRecord::Base
      self.primary_key = [:id_1, :id_2]
    end
    
    Order.find_each(order: [:asc, :desc])
    

    在上面的代码中,id_1 按升序排序,id_2 按降序排序。

限制会被遵守,如果存在,则对批次大小没有要求:它可以小于、等于或大于限制。

startfinish 选项在您希望多个工作程序处理同一个处理队列时特别有用。您可以通过设置每个工作程序的 :start:finish 选项来让工作程序 1 处理从 id 1 到 9999 之间的所有记录,而工作程序 2 处理从 10000 及以后的记录。

# In worker 1, let's process until 9999 records.
Person.find_each(finish: 9_999) do |person|
  person.party_all_night!
end

# In worker 2, let's process from record 10_000 and onwards.
Person.find_each(start: 10_000) do |person|
  person.party_all_night!
end

注意:排序可以是升序(:asc)或降序(:desc)。它会自动设置为主键的升序(“id ASC”)。这也意味着此方法仅在游标列可排序时才有效(例如,整数或字符串)。

注意:在使用自定义列进行批处理时,它们应该包含至少一个唯一列(例如主键)作为 tiebreaker。此外,为了减少出现竞争条件的可能性,所有列都应该是静态的(在设置后不可更改)。

注意:根据其本质,如果其他进程正在修改数据库,批处理会受到竞争条件的影响。

# File activerecord/lib/active_record/relation/batches.rb, line 85
def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, &block)
  if block_given?
    find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do |records|
      records.each(&block)
    end
  else
    enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do
      relation = self
      cursor = Array(cursor)
      apply_limits(relation, cursor, start, finish, build_batch_orders(cursor, order)).size
    end
  end
end

find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER)

根据查找选项作为数组返回的每批记录生成。

Person.where("age > 21").find_in_batches do |group|
  sleep(50) # Make sure it doesn't get too crowded in there!
  group.each { |person| person.party_all_night! }
end

如果您不为 find_in_batches 提供块,它将返回一个枚举器,用于与其他方法链式调用。

Person.find_in_batches.with_index do |group, batch|
  puts "Processing group ##{batch}"
  group.each(&:recover_from_last_night!)
end

要逐个生成每条记录,请使用 find_each

选项

  • :batch_size - 指定批次的大小。默认为 1000。

  • :start - 指定要开始的游标列值,包括该值。

  • :finish - 指定要结束的游标列值,包括该值。

  • :error_on_ignore - 覆盖应用程序配置以指定在关联中存在排序时是否应引发错误。

  • :cursor - 指定用于批处理的列(可以是列名或列名数组)。默认为主键。

  • :order - 指定游标列的顺序(可以是 :asc:desc 或包含 :asc 或 :desc 的数组)。默认为 :asc

    class Order < ActiveRecord::Base
      self.primary_key = [:id_1, :id_2]
    end
    
    Order.find_in_batches(order: [:asc, :desc])
    

    在上面的代码中,id_1 按升序排序,id_2 按降序排序。

限制会被遵守,如果存在,则对批次大小没有要求:它可以小于、等于或大于限制。

startfinish 选项在您希望多个工作程序处理同一个处理队列时特别有用。您可以通过设置每个工作程序的 :start:finish 选项来让工作程序 1 处理从 id 1 到 9999 之间的所有记录,而工作程序 2 处理从 10000 及以后的记录。

# Let's process from record 10_000 on.
Person.find_in_batches(start: 10_000) do |group|
  group.each { |person| person.party_all_night! }
end

注意:排序可以是升序(:asc)或降序(:desc)。它会自动设置为主键的升序(“id ASC”)。这也意味着此方法仅在游标列可排序时才有效(例如,整数或字符串)。

注意:在使用自定义列进行批处理时,它们应该包含至少一个唯一列(例如主键)作为 tiebreaker。此外,为了减少出现竞争条件的可能性,所有列都应该是静态的(在设置后不可更改)。

注意:根据其本质,如果其他进程正在修改数据库,批处理会受到竞争条件的影响。

# File activerecord/lib/active_record/relation/batches.rb, line 161
def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER)
  relation = self
  unless block_given?
    return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do
      cursor = Array(cursor)
      total = apply_limits(relation, cursor, start, finish, build_batch_orders(cursor, order)).size
      (total - 1).div(batch_size) + 1
    end
  end

  in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do |batch|
    yield batch.to_a
  end
end

in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, use_ranges: nil, &block)

生成 ActiveRecord::Relation 对象以处理一批记录。

Person.where("age > 21").in_batches do |relation|
  relation.delete_all
  sleep(10) # Throttle the delete queries
end

如果您不为 in_batches 提供块,它将返回一个 BatchEnumerator,它是可枚举的。

Person.in_batches.each_with_index do |relation, batch_index|
  puts "Processing relation ##{batch_index}"
  relation.delete_all
end

调用返回的 BatchEnumerator 对象的方法示例

Person.in_batches.delete_all
Person.in_batches.update_all(awesome: true)
Person.in_batches.each_record(&:party_all_night!)

选项

  • :of - 指定批次的大小。默认为 1000。

  • :load - 指定是否应加载关联。默认为 false。

  • :start - 指定要开始的游标列值,包括该值。

  • :finish - 指定要结束的游标列值,包括该值。

  • :error_on_ignore - 覆盖应用程序配置以指定在关联中存在排序时是否应引发错误。

  • :cursor - 指定用于批处理的列(可以是列名或列名数组)。默认为主键。

  • :order - 指定游标列的顺序(可以是 :asc:desc 或包含 :asc 或 :desc 的数组)。默认为 :asc

    class Order < ActiveRecord::Base
      self.primary_key = [:id_1, :id_2]
    end
    
    Order.in_batches(order: [:asc, :desc])
    

    在上面的代码中,id_1 按升序排序,id_2 按降序排序。

  • :use_ranges - 指定是否使用范围迭代(id >= x AND id <= y)。它可以使对整个表格或几乎整个表格的多次迭代更快。默认情况下,只有整个表格迭代使用这种迭代方式。您可以通过传递 false 来禁用此行为。如果您对表格进行迭代,并且唯一的条件是,例如,archived_at: nil(并且只有很小一部分记录已归档),那么选择这种方法是有意义的。

限制会被遵守,如果存在,则对批次大小没有要求,它可以小于、等于或大于限制。

startfinish 选项在您希望多个工作程序处理同一个处理队列时特别有用。您可以通过设置每个工作程序的 :start:finish 选项来让工作程序 1 处理从 id 1 到 9999 之间的所有记录,而工作程序 2 处理从 10000 及以后的记录。

# Let's process from record 10_000 on.
Person.in_batches(start: 10_000).update_all(awesome: true)

在关联上调用 where 查询方法的示例

Person.in_batches.each do |relation|
  relation.update_all('age = age + 1')
  relation.where('age > 21').update_all(should_party: true)
  relation.where('age <= 21').delete_all
end

注意:如果您要遍历每条记录,则应该在生成的 BatchEnumerator 上调用 each_record

Person.in_batches.each_record(&:party_all_night!)

注意:排序可以是升序(:asc)或降序(:desc)。它会自动设置为主键的升序(“id ASC”)。这也意味着此方法仅在游标列可排序时才有效(例如,整数或字符串)。

注意:在使用自定义列进行批处理时,它们应该包含至少一个唯一列(例如主键)作为 tiebreaker。此外,为了减少出现竞争条件的可能性,所有列都应该是静态的(在设置后不可更改)。

注意:根据其本质,如果其他进程正在修改数据库,批处理会受到竞争条件的影响。

# File activerecord/lib/active_record/relation/batches.rb, line 259
def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, use_ranges: nil, &block)
  cursor = Array(cursor).map(&:to_s)
  ensure_valid_options_for_batching!(cursor, start, finish, order)

  if arel.orders.present?
    act_on_ignored_order(error_on_ignore)
  end

  unless block
    return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, cursor: cursor, order: order, use_ranges: use_ranges)
  end

  batch_limit = of

  if limit_value
    remaining   = limit_value
    batch_limit = remaining if remaining < batch_limit
  end

  if self.loaded?
    batch_on_loaded_relation(
      relation: self,
      start: start,
      finish: finish,
      cursor: cursor,
      order: order,
      batch_limit: batch_limit,
      &block
    )
  else
    batch_on_unloaded_relation(
      relation: self,
      start: start,
      finish: finish,
      load: load,
      cursor: cursor,
      order: order,
      use_ranges: use_ranges,
      remaining: remaining,
      batch_limit: batch_limit,
      &block
    )
  end
end