`find` の order + offset が ids を超えるケースでの RecordNotFound 発生を修正

rails/rails

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 の正当なケースはそのまま保持しています。

まとめ

orderoffset が IDs の総数を超えると発生していた spurious な RecordNotFound が、find_some のオフセット処理に guard 条件を追加するだけで解消されました。これにより、find の ordered と unordered の挙動が統一され、開発者が期待する「範囲外のページングは空配列」を得られるようになりました。追加されたテストにより、主要データベース全てで回帰が防がれています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
fac73a64

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの5部構成が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは ````ruby:ファイルパス```` 形式で正しく記載され、PRリンクは #番号 形式でリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Rails の内部実装に関する記述で、専門的なエンジニア向けの内容になっており、不要な基礎解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論パラグラフで要旨を示し、以降の段落は1トピック・短め(6文未満)で構成され、空行で区切られています。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

変更後のコードはDiffと完全に一致していますが、変更前のコードブロックに余分な `end` が含まれており、Diffからは省略されています。機能上の影響は無いものの、正確性の観点で修正が推奨されます。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

RecordNotFound、expected_size、offset、limit などの用語はPRと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

原因・修正内容・テスト追加の説明がPRの記述と合致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の主張はすべてPRのタイトル・説明・Diffで裏付けられており、捏造や推測はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #57598 だけが数値で使用されており、正しく記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

Rails のバージョンサポートやリリース日程といった外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PRの記述と一致しています。