複合外部キーを持つ `belongs_to` バリデーション条件の修正
belongs_to_required_validates_foreign_key が false の場合、複合外部キーを持つ belongs_to のバリデーション条件が常に true となり、最適化が無効化されていたバグが修正されました。これにより、複合外部キーのカラムが変更されていない場合には親レコードの存在確認がスキップされる正しい挙動になります。
背景
belongs_to_required_validates_foreign_key を false(load_defaults(7.1) 以降のデフォルト値)にすると、Railsは必須 belongs_to アソシエーションに条件付きプレゼンスバリデーターを設置します。この条件ラムダは、外部キーが nil であるか変更された場合のみバリデーションを実行することで、外部キーが存在し変更されていない場合に親レコードのロードを回避する最適化として機能します。
しかし、複合外部キー(foreign_key: [:shop_id, :order_id] のような配列)を使用する場合、この条件ロジックが正しく動作しませんでした。条件ラムダ内の record.read_attribute(foreign_key) は引数に対して .to_s を呼び出します。foreign_key が配列の場合、.to_s は '["shop_id", "order_id"]' のような文字列を生成しますが、これはどの属性名とも一致しないため、read_attribute は常に nil を返していました。結果として条件は常に true となり、バリデーションは常に実行されていました。
この問題が実際に引き起こす障害は深刻です。default_scope でソフトデリートを実装しているモデル(例: where(is_not_deleted: true))では、ソフトデリートされた子レコードがソフトデリートされた親レコードを参照している場合、外部キーカラムが一切変更されていなくても default_scope を通じて親レコードが見つからず、バリデーションエラーが発生するという問題がありました。
技術的な変更
activerecord/lib/active_record/associations/builder/belongs_to.rb の define_validations メソッド内の条件ラムダが修正され、複合外部キーの各カラムを個別にチェックするようになりました。
変更前:
record.read_attribute(foreign_key).nil? ||
record.attribute_changed?(foreign_key) ||
(reflection.polymorphic? && (record.read_attribute(foreign_type).nil? || record.attribute_changed?(foreign_type)))
変更後:
fk_missing_or_changed = if foreign_key.is_a?(Array)
foreign_key.any? { |fk| record.read_attribute(fk).nil? || record.attribute_changed?(fk) }
else
record.read_attribute(foreign_key).nil? ||
record.attribute_changed?(foreign_key)
end
fk_missing_or_changed ||
(reflection.polymorphic? && (record.read_attribute(foreign_type).nil? || record.attribute_changed?(foreign_type)))
修正後の挙動は次のとおりです:
- すべてのFKカラムが存在し、いずれも変更されていない場合 → バリデーションをスキップ
- いずれかのFKカラムが
nilの場合 → バリデーションを実行 - いずれかのFKカラムが変更された場合 → バリデーションを実行
テストは activerecord/test/cases/associations/belongs_to_associations_test.rb に3ケースが追加されました。また、activerecord/test/models/cpk/book.rb に Cpk::BookWithRequiredOrder モデルが追加され、optional: false の複合外部キー belongs_to を持つテスト用モデルが整備されています。
設計判断
is_a?(Array) による分岐で既存の単一外部キーの処理を維持する方式 が採用されました。
複合外部キーを配列の any? で走査するアプローチは、「いずれかのカラムが欠落・変更されていれば検証を行う」という保守的な判断です。これは単一外部キーの場合の意味論と一致しており、部分的な変更(例: shop_id のみ変更)でも確実にバリデーションが実行される安全側の設計です。また、スカラー外部キーのコードパスは変更されていないため、既存の動作への影響はありません。
ポリモーフィックアソシエーションの foreign_type チェックは fk_missing_or_changed の後段に配置されており、複合外部キーとポリモーフィックアソシエーションが競合するケースへの影響も最小限に抑えられています。
まとめ
本PRは、read_attribute が配列引数を正しく処理できないという根本原因を、最小限のコード変更で修正したバグフィックスです。ソフトデリートパターンと複合外部キーを組み合わせたモデルで不当なバリデーションエラーが発生していたケースが解消され、複合外部キーにおける条件付きバリデーションの最適化が設計通りに機能するようになります。