属性エイリアスに対する `update_attribute` の読み取り専用チェックを修正
update_attribute と update_attribute! が、エイリアスで参照された読み取り専用属性に対して例外を発生させなかった不整合を解消し、update_columns と同様のエイリアス解決ロジックを導入しました。これにより、属性エイリアス経由でも一貫した例外挙動が保証されます。
背景
update_attribute 系列は属性エイリアスを解決せずに読み取り専用チェックを行っていました。verify_readonly_attribute(name) が内部で _attr_readonly.include?(name) を参照しますが、_attr_readonly は正規のカラム名だけを保持しているため、エイリアス名が渡されるとガードがスキップされ、例外が発生しませんでした。この挙動は、raise_on_assign_to_attr_readonly が無効(デフォルト)な環境で特に問題となり、メモリ上の変更は成功したもののデータベースへの永続化が行われず true が返されるという不整合を引き起こしていました。
技術的な変更
activerecord/lib/active_record/persistence.rb の update_attribute と update_attribute! に対し、エイリアス解決の一行を追加しました。
def update_attribute(name, value)
name = name.to_s
+ name = self.class.attribute_aliases[name] || name
verify_readonly_attribute(name)
public_send("#{name}=", value)
save(validate: false)
end
def update_attribute!(name, value)
name = name.to_s
+ name = self.class.attribute_aliases[name] || name
verify_readonly_attribute(name)
public_send("#{name}=", value)
save!
end
エイリアスが存在しない場合は nil が返り、元の name がそのまま使用されるため既存ロジックへの影響はありません。update_columns がすでに採用していた self.class.attribute_aliases[name] || name と同一の実装に揃えることで、コードベースの一貫性も向上しています。
設計判断
エイリアス解決を既存メソッドへ統合する方針 が採用されました。別途新しい設定キーやフラグを導入する代わりに、update_columns と同等のロジックをコピーすることで、最小限の変更で期待する動作を実現しています。PR の議論ではエイリアス自体を読み取り専用属性として扱うかどうかが検討されましたが、今回の変更は「正規属性が読み取り専用である限り、エイリアス経由でも同様に例外を投げる」という内部整合性の修正に留められ、既存の設計哲学を崩さない選択となっています。
まとめ
update_attribute 系列にエイリアス解決を追加したことで、読み取り専用属性への書き込みがエイリアス経由でも例外を発生させるようになり、update_columns と同等の安全性が確保されました。最小限のコード追加で一貫性を回復し、フレームワークの期待動作を保ちつつ既存ユーザーへの影響を回避しています。