update_all がエイリアス属性でロックカラムを壊す問題を修正
update_all が alias_attribute を介して楽観ロックカラムを設定した場合、SQLite/MySQL ではユーザが指定した値が失われ、PostgreSQL では duplicate column エラーが発生していました。本修正はその挙動を正しく扱えるようにし、全 DB アダプタで一貫した結果を得られるようにします。
背景
update_all がエイリアス属性でロックカラムを更新できない というバグは、alias_attribute :v, :lock_version のように別名でロックカラムを参照した際に顕在化します。SQLite/MySQL では UPDATE 文に同一カラムへの二重代入が生成され、後者の代入(自動インクリメント)が優先されてユーザ指定の 10 が 1 に上書きされ、PostgreSQL では「multiple assignments to same column」の構文エラーが送出されました。原因は update_all 内のロックカラム判定がカラム名の 正規名(lock_version)だけを比較対象としており、エイリアスキー (v) が等価とみなされなかったことにあります。
技術的な変更
判定ロジックのエイリアス解決 を導入しました。activerecord/lib/active_record/relation.rb の該当箇所では、従来の !updates.key?(model.locking_column) && !updates.key?(model.locking_column.to_sym) を、updates.keys.none? { |key| (model.attribute_alias(key) || key.to_s) == model.locking_column } に置き換え、各キーを model.attribute_alias で正規名に変換して比較するようにしました。これによりエイリアスでもロックカラムが認識され、自動インクリメントが不必要に付加されなくなります。
@@
- if model.locking_enabled? &&
- !updates.key?(model.locking_column) &&
- !updates.key?(model.locking_column.to_sym)
+ if model.locking_enabled? &&
+ updates.keys.none? { |key| (model.attribute_alias(key) || key.to_s) == model.locking_column }
テストケース も追加しました。activerecord/test/cases/locking_test.rb に PersonWithAliasedLockVersion というエイリアス付きモデルを作成し、update_all(aliased_lock_version: 10) が正しく lock_version を 10 に更新し、1 行だけ影響したことを検証するテストを導入しています。
class PersonWithAliasedLockVersion < ActiveRecord::Base
self.table_name = :people
alias_attribute :aliased_lock_version, :lock_version
end
# ...
def test_update_all_with_lock_column_set_through_an_alias
person = PersonWithAliasedLockVersion.create!(first_name: "aliased")
assert_equal 0, person.lock_version
affected = PersonWithAliasedLockVersion.where(id: person.id)
.update_all(aliased_lock_version: 10)
assert_equal 1, affected
assert_equal 10, person.reload.lock_version
end
CHANGELOG には「Fix update_all corrupting the optimistic locking column when it is set through an alias_attribute.」というエントリを追加し、変更の概要を記載しています。
設計判断
この修正は insert_all が既に採用しているエイリアス解決ロジック を update_all に流用するという最小限の侵入で実装されています。新たな設定項目や API の追加は行わず、既存のロジックに対してキー比較だけを拡張することで、従来コードとの互換性をそのまま保ちつつバグを解消できました。エイリアス解決はモデル層の責務であるため、データベースアダプタ側への変更は不要です。
まとめ
update_all がエイリアス属性でロックカラムを更新した際に起きていたデータ損失と構文エラーを、キー比較時に エイリアスを正規名に変換 するだけのシンプルな変更で解決しました。テストと CHANGELOG の追加により挙動の保証と変更履歴の明示が行われ、全ての主要データベースで安全に楽観ロックが利用できるようになっています。