`BatchEnumerator`に`cursor`・`order`・`use_ranges`属性を追加
BatchEnumeratorが保持する全属性に外部からアクセスできるようになりました。cursor・order・use_rangesの3つの属性リーダーが新たに公開されたことで、BatchEnumeratorオブジェクトを受け取るライブラリやgemがバッチ処理の設定を完全に参照できます。
背景
BatchEnumeratorは、ActiveRecord::Batches#in_batchesをブロックなしで呼び出したときに返されるオブジェクトで、バッチ処理に関するパラメータを保持します。#42312でstart・finish・relation・batch_sizeが既に公開されていましたが、cursor・order・use_rangesの3つは内部にとどまったままでした。
PR著者が開発するgemでは、アプリケーションコードから渡されたBatchEnumeratorを受け取り、その属性に基づいてロジックを分岐させる必要がありました。しかし未公開の属性にアクセスする手段がなく、実装が困難な状況でした。今回の変更はその制約を解消するフォローアップです。
技術的な変更
batch_enumerator.rbへの属性リーダーの追加が中心的な変更ですが、batches.rb側でもcursorの変換タイミングに関する修正が行われています。
batch_enumerator.rbへの属性追加:
# 変更前: cursor, order, use_ranges は attr_reader なし
# 変更後
# The column to use for batching.
attr_reader :cursor
# The cursor column order.
attr_reader :order
# Specifies whether to use range iteration (id >= x AND id <= y).
attr_reader :use_ranges
あわせて、startとfinishのコメントが「primary key value」から「cursor column value」に修正されました。これはcursorが主キー以外の列を指定できる現状の仕様をより正確に反映したものです。
batches.rbでのcursor変換タイミングの調整:
# 変更前: BatchEnumerator生成前に cursor を String 配列へ変換
def in_batches(...)
cursor = Array(cursor).map(&:to_s) # ← ここで変換
ensure_valid_options_for_batching!(cursor, start, finish, order)
if arel.orders.present?
# ...
return BatchEnumerator.new(..., cursor: cursor, ...) # 変換済みの値が渡される
end
# ...
end
# 変更後: BatchEnumerator には変換前の値を渡し、変換は後続処理の直前へ移動
def in_batches(...)
ensure_valid_options_for_batching!(cursor, start, finish, order)
if arel.orders.present?
# ...
return BatchEnumerator.new(..., cursor: cursor, ...) # 変換前の値が渡される
end
cursor = Array(cursor).map(&:to_s) # ← BatchEnumerator生成後に変換
# ...
end
private
def ensure_valid_options_for_batching!(cursor, start, finish, order)
cursor = Array(cursor).map(&:to_s) # ← バリデーション内で変換
# ...
end
この変更は、cursor属性の公開と深く関係しています。変換前にBatchEnumeratorが生成されていた場合、外部から取得したcursorの値は常に文字列配列(例:["id"])になってしまいます。変換タイミングを後ろにずらすことで、BatchEnumeratorには呼び出し側が渡した元の値(例::idや"id"のまま)が保持されるようになります。
テストコードも新しい属性の検証を含むよう更新されています:
def test_in_batches_has_attribute_readers
enumerator = Post.no_comments.in_batches(of: 2, start: 42, finish: 84, use_ranges: true)
assert_equal Post.no_comments, enumerator.relation
assert_equal 2, enumerator.batch_size
assert_equal 42, enumerator.start
assert_equal 84, enumerator.finish
assert_equal "id", enumerator.cursor # 新規
assert_equal :asc, enumerator.order # 新規
assert_equal true, enumerator.use_ranges # 新規
end
設計判断
cursorをどのような型で公開するかという点が、今回の変更で最も注意深く設計された部分です。
変更前はBatchEnumerator生成前にArray(cursor).map(&:to_s)を実行していたため、たとえ呼び出し元がシンボル:idを渡していても、BatchEnumerator内部では["id"]という文字列配列として保持されていました。公開後にこの変換済みの値が外部に露出すると、呼び出し元の渡し方と異なる型が返ってくるという不整合が生じます。変換タイミングをBatchEnumerator生成の後ろに移動させることで、この問題を回避しています。
バリデーション用のensure_valid_options_for_batching!メソッドも内部で独自に変換を行うよう修正されており、変換処理が重複していますが、各処理が自身のスコープで必要な形に変換する明確な設計になっています。
まとめ
今回の変更は、BatchEnumeratorが保持する全属性を一貫したインターフェースで公開し、gemやライブラリがバッチ処理の設定を完全に参照できるようにしたものです。cursor変換タイミングの調整によって属性値の型整合性が保たれており、シンプルな属性追加の裏に慎重な設計判断が含まれています。