複合主キーモデルで `find_signed` が `ArgumentError` を発生させるバグを修正

rails/rails

複合主キーを持つモデルで find_signed を呼び出すと ArgumentError が発生するバグが修正されました。1行の変更により、find_signedfind_signed!find と一貫した動作をするようになります。

背景

find_signed は複合主キー(CPK)モデルに対して ArgumentError を発生させていました。マジックサインインリンク、パスワードリセット、メール確認などで複合主キーモデルの find_signed を使用しているケースで問題が生じていました。

再現コードは以下のとおりです:

class Cpk::Order < ActiveRecord::Base
  self.primary_key = [:shop_id, :id]
end

order = Cpk::Order.first
token = order.signed_id

Cpk::Order.find_signed!(token)
# => #<Cpk::Order id: 75294940, shop_id: 37647470, ...>

Cpk::Order.find_signed(token)
# => ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array

find_signed! は正常に動作するにもかかわらず、find_signed だけが失敗するという非対称な挙動が問題でした。

根本原因は signed_id.rb 内の find_by primary_key => id という呼び出しにあります。複合主キーの場合、primary_key["shop_id", "id"] という配列、id[37647470, 75294940] という配列になります。predicate_builder.rb は配列のキーを「複数レコードの検索」と解釈するため、値が配列の配列([[shop_id, id], [shop_id, id], ...])であることを期待します。値が配列でない要素を見つけると ArgumentError を発生させます。一方、find_signed! が使用する find(id)find_one を経由し、finder_methods.rbprimary_keyidzip で結合するため、この問題を回避できていました。

なお、同種のバグは2024年9月に find_by_token_for に対して 8617a7c(@fatkodima 氏)で修正されています。token_for.rb では find_by(model.primary_key => id)find_by(model.primary_key => [id]) に変更されましたが、同一のパターンを持つ signed_id.rb はその時点で更新されませんでした。

技術的な変更

activerecord/lib/active_record/signed_id.rb の1行変更により、id を配列で包むことで predicate builder が単一の複合キータプルとして解釈できるようにしました。

変更前:

if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
  find_by primary_key => id
end

変更後:

if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
  find_by(primary_key => [id])
end

[id] と包むことで、predicate builder は「1つの複合キータプルにマッチするレコードを検索」と解釈します。単一主キーの場合は primary_key が文字列、id がスカラー値なので、[id] は単一要素の配列となり従来と同じ動作をします。

テストは activerecord/test/cases/signed_id_test.rb に3つ追加されています:

  • CPKモデルに対する find_signed の往復テスト
  • リレーションスコープ付きCPKモデルでの find_signed(ポジティブ・ネガティブスコープの両方)
  • CPKモデルに対する find_signed! の往復テスト(リグレッションガード)

設計判断

最終的にマージされたコードでは、過去の類似修正(token_for.rb8617a7c)と同様に find_by(primary_key => [id]) という配列でラップする方式 が採用されました。

PR本文では primary_key.zip(id).to_h というアプローチも検討されており、このアプローチは { shop_id: 37647470, id: 75294940 } という明示的な条件ハッシュを生成し、finder_methods.rbfind_one と同じパターンで可読性が高い点が利点として挙げられています。PR本文では zip アプローチが推奨されていますが、最終的なコードでは [id] でラップする方式が採用されています。どちらのアプローチも最終的に同じSQLクエリを生成するため、動作上の違いはありません。

find ではなく find_by を維持している点も重要な判断です。find に切り替えると SignedId::RelationMethods#find_signed によるリレーションスコープ(例: Cpk::Order.where(...).find_signed(token))が機能しなくなります。find_by を使うことで、スコープ付きのクエリが引き続き正しく動作します。

まとめ

本PRは、token_for.rb で2024年9月に適用された同種の修正が signed_id.rb に漏れていた問題を解消するものです。1行の変更で find_signed の動作が find_signed!find と一貫するようになり、複合主キーモデルを使用したサインインリンクやパスワードリセット機能が正しく動作するようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
09f55ca2

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という構成が明確で、ガイドラインを完全に満たしています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:filepath)、コミットIDの短縮リンク、PR番号のリンクなど、すべてのカスタムMarkdown構文が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Railsの内部実装(predicate builderなど)に言及しており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論→各論で構成され、各パラグラフもトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロック(変更前・変更後)は、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「複合主キー(CPK)」「predicate builder」「リレーションスコープ」など、PR内で使用されている技術用語を正確かつ適切に使用しています。

説明の技術的正確性 ✓ PASS

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

バグの根本原因(predicate builderの解釈)から修正による挙動の変化まで、PRの内容に基づいて技術的に正確な説明がなされています。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescription、Diff、関連コミット情報によって裏付けられています。特に、PR作成者が提案したアプローチと最終的に採用されたアプローチの違いを「設計判断」として正確に記述しており、ハルシネーションは見られません。

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

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

PR番号(#57245)、コミットID(8617a7c)など、記事に含まれるすべての数値・固有名詞は正確です。

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

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

記事のタイトル「複合主キーモデルで `find_signed` が `ArgumentError` を発生させるバグを修正」は、PRのタイトル「Fix `find_signed` for models with a composite primary key」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事内の情報はすべてPRで提供された情報に基づいており、PRに記載のない外部知識(バージョンサポート状況など)の持ち込みはありません。「2024年9月」という言及もPR内の記述に基づいています。

時間表現の正確性 ✓ PASS

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

過去の修正について「修正されています」や「更新されませんでした」といった時間表現が、PR内の文脈と一致しており正確です。