update_all がエイリアス属性でロックカラムを壊す問題を修正

rails/rails

update_allalias_attribute を介して楽観ロックカラムを設定した場合、SQLite/MySQL ではユーザが指定した値が失われ、PostgreSQL では duplicate column エラーが発生していました。本修正はその挙動を正しく扱えるようにし、全 DB アダプタで一貫した結果を得られるようにします。

背景

update_all がエイリアス属性でロックカラムを更新できない というバグは、alias_attribute :v, :lock_version のように別名でロックカラムを参照した際に顕在化します。SQLite/MySQL では UPDATE 文に同一カラムへの二重代入が生成され、後者の代入(自動インクリメント)が優先されてユーザ指定の 101 に上書きされ、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.rbPersonWithAliasedLockVersion というエイリアス付きモデルを作成し、update_all(aliased_lock_version: 10) が正しく lock_version10 に更新し、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 の追加により挙動の保証と変更履歴の明示が行われ、全ての主要データベースで安全に楽観ロックが利用できるようになっています。

記事メタデータ

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

この記事は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)で使用され、PRリンクも正しくマークダウン化されています。

対象読者への適合性 ✓ PASS

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

内容はRailsのコア開発者向けで、不要な初学者向け説明はなく、専門的な読者に適しています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと完全に一致し、ファイル名・変更内容も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語はPRで用いられているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

バグの原因と修正内容がPRの説明と一致しており、論理的に正しい記述です。

事実の突合 ✓ PASS

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

記事の全ての主張はPRタイトル・Description・Diffで裏付けられ、ハルシネーションはありません。

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

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

PR番号 #57606 だけが記載されており、正確です。その他数値的な記載はありません。

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

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

記事タイトルはPRの内容を正確に要約しており、一致しています。

外部知識の正確性 ✓ PASS

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

PRに記載されていない外部知識(バージョンサポート等)は含まれていません。

時間表現の正確性 ✓ PASS

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

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