normalizes におけるインプレース変更検出のキャストバグ修正

rails/rails

この PR は、normalizes がデータベースから取得した生の値を比較対象にした際に発生していた例外を解消し、インプレース変更検出の正確性を復元します。

背景

インプレース変更検出が正しく機能しない回帰 が #57639 によって導入されました。normalized_attribute_changed_in_place? は現在の値と attribute.type.cast(attribute.value_before_type_cast) を比較していましたが、DB から構築された属性では value_before_type_cast がシリアライズされた生文字列になるため、cast が期待するデシリアライズが行われませんでした。たとえば JSON カラムの場合、生の JSON 文字列が cast に渡され、ユーザー定義の正規化ブロックが文字列に対して呼び出され NoMethodError が発生していました。

結果として正規化が失敗し、レコードの保存やバリデーションができなくなっていました。この問題は、デフォルト値が設定された新規レコードでも同様に起き、JSON 以外のシリアライズ型でも同様のリスクがありました。そこで、比較ロジックを DB 由来の値でも適切にデシリアライズできる形に変更する必要がありました。

技術的な変更

normalized_attribute_changed_in_place?attribute.type_cast を使用するように変更されました。以下の差分が適用されています。

@@ -145,7 +145,7 @@ def normalize_changed_in_place_attributes
         end

         def normalized_attribute_changed_in_place?(attribute)
-          attribute.changed_in_place? && attribute.value != attribute.type.cast(attribute.value_before_type_cast)
+          attribute.changed_in_place? && attribute.value != attribute.type_cast(attribute.value_before_type_cast)
         end

attribute.type_cast は内部で type.deserialize(DB から来た場合)または type.cast(ユーザーが割り当てた場合)を呼び出すため、比較対象が正しくデシリアライズされます。この変更により、既存の true/false 判定ロジックはそのまま維持され、ユーザーが明示的に値を設定したケースでは従来通り cast が使用されます。

テストが追加され、修正の効果が検証されていますNormalizedAdminUser というテストモデルを作成し、JSON カラム json_optionsnormalizes を適用しました。レコードを DB から取得後にインプレースでキーを追加し validate を呼び出すと、正規化されたハッシュが期待通り取得できることを確認しています。

class NormalizedAdminUser < ActiveRecord::Base
  self.table_name = "admin_users"
  attribute :json_options, :json
  normalizes :json_options, with: -> options { options.transform_keys(&:downcase) }
end

test "normalizes json attribute changed in place after loading from database" do
  admin_user = NormalizedAdminUser.find(NormalizedAdminUser.create!(json_options: { "FOO" => "bar" }).id)
  admin_user.json_options["BAZ"] = "qux"
  admin_user.validate
  assert_equal({ "foo" => "bar", "baz" => "qux" }, admin_user.json_options)
end

このテストは、JSON だけでなくデフォルト値が設定されたカラムでも同様に機能することを示しています。

設計判断

既存の型システムを活用して最小限の変更で問題を解決 する方針が採られました。type_cast は型オブジェクトの公開 API であり、デシリアライズとキャストの切り替えロジックを内部に持っています。新たな分岐や特別ケースを追加する代わりに、このメソッドを呼び出すだけで正しい比較が実現でき、既存コードへの影響範囲が極小に抑えられました。

後方互換性が維持されています。ユーザーが手動で属性に値を代入した場合、type.cast がそのまま使われるため、以前の振る舞いに変化はありません。したがって、外部からの API 変更やマイグレーション作業は不要です。

まとめ

この PR は、normalizes がデータベース由来のシリアライズ値を正しくデシリアライズして比較できるようにし、インプレース変更検出の回帰を修正しました。attribute.type_cast への置き換えは既存の型システムを活かす設計選択であり、機能追加や破壊的変更を伴わずにバグを解消しています。テストの追加により、JSON カラムだけでなくデフォルト付きカラムでも安定した正規化が保証されます。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断(任意)及びまとめが明確に区分されており、総論→各論→結論の流れが保たれています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライトは正しい形式(```ruby:filepath)で記述され、diffブロックも正しくマークダウン化されています。PRリンクは ``[PR #57660](URL)`` でリンク化されており、要件を満たしています。

対象読者への適合性 ✓ PASS

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

Rails の内部実装や ActiveModel の型システムに関する記述が中心で、エンジニア向けの適切な技術レベルです。初心者向けの過度な説明はありません。

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

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

各セクションは総論パラグラフ、詳細パラグラフ、結論パラグラフで構成され、段落はトピックセンテンスで始まり1段落1テーマ、長さも適切です。段落間は空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のdiffブロックとテストコードは、提供されたPR Diff と一致しています。抜粋部分はDiff内に存在し、内容の改変や省略はなく正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`normalized_attribute_changed_in_place?`, `type_cast`, `deserialize` などの用語はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの背景・詳細と整合しており、因果関係やロジックに矛盾はありません。

事実の突合 ✓ PASS

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

記事の全ての主張はPRタイトル、Description、Diffで裏付けられており、憶測や外部知識の付加はありません。

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

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

PR番号 #57660 と Issue番号 #57639 が正確に記載されており、その他数値的な記述はありません。

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

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

記事タイトルはPRの趣旨(`normalizes` のキャストバグ修正)を日本語で正確に表現しています。

外部知識の正確性 ✓ PASS

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

記事はPRに記載された情報のみで構成され、バージョンサポートやリリース情報等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

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