`find` の order + offset が ids を超えるケースでの RecordNotFound 発生を修正
find に配列の ID と明示的な order、さらに offset が ID の数を超えると、負の件数を示す ActiveRecord::RecordNotFound がスローされていました。本稿では、原因となるロジックと修正内容、そして設計上の判断ポイントを整理します。
背景
find の ordered パス は order が指定されていると find_some メソッドへ委譲され、offset の計算結果が負になると期待件数 (expected_size) が負のまま残ります。負の期待件数と実際の取得件数 (0) が一致しないため、Rails は spurious な RecordNotFound を例外として送出します。order を付けない場合は find_some_ordered が走り、同様の入力で空配列が返されるため、二つの経路で結果が不整合となっていました。
技術的な変更
変更前のロジック
expected_size =
if limit_value && ids.size > limit_value
limit_value
else
ids.size
end
if offset_value && (ids.size - offset_value < expected_size)
expected_size = ids.size - offset_value
end
ids.size - offset_value が負になると expected_size が負になる点が根本原因です。
修正後のロジック
if offset_value
if ids.size <= offset_value
return []
elsif ids.size - offset_value < expected_size
expected_size = ids.size - offset_value
end
end
-
ids.size <= offset_valueの場合は即座に空配列を返すことで負のサイズを防止。 - それ以外は従来通り
expected_sizeを調整し、既存のlimit+offsetの境界条件 (ids.size - offset_value < expected_size) には影響を与えません。
テスト強化
finder_test.rb に二つの回帰テストが追加され、order 付き・なし両方で offset が ids を超える シナリオと、複数の limit/offset 組み合わせが同一結果になることを検証しています。SQLite、PostgreSQL、MySQL3 すべてでテストが緑になることが確認されています。
設計判断
この修正は 「期待件数を 0 でクランプ」 するシンプルな防御策を採用し、既存のロジックを最小限の変更に留めています。offset が過剰な場合に空配列を返すという動作は、find_some_ordered と一致させることで API の一貫性を確保しました。新たに別メソッドやフラグを導入せず、既存コードパスへの侵入を抑制しつつ、既存の limit + offset の正当なケースはそのまま保持しています。
まとめ
order と offset が IDs の総数を超えると発生していた spurious な RecordNotFound が、find_some のオフセット処理に guard 条件を追加するだけで解消されました。これにより、find の ordered と unordered の挙動が統一され、開発者が期待する「範囲外のページングは空配列」を得られるようになりました。追加されたテストにより、主要データベース全てで回帰が防がれています。