`alias_attribute`の重複エントリ蓄積によるO(N²)劣化を修正
ActiveModel::AttributeMethods#alias_attributeが同一エイリアスの重複を排除せずに追記し続ける問題を、1行の変更で修正しました。これにより、table_nameを繰り返し変更するActiveRecordクラスでクエリコストがO(N²)で増大するリグレッションが解消されます。
背景
Rails 7.1で導入された2つの変更の組み合わせが、クエリコストの単調増加を引き起こしていました。
1つ目は、e90b11e による変更です。ActiveRecord::Base#load_schema!が、idカラムを持つすべてのスキーマのロード時に alias_attribute :id_value, :id を自動呼び出しするようになりました。これは複合主キーを持つモデルで生のidカラム値にアクセスするための#id_valueエイリアスを提供するものでした。
2つ目は、0f5563b による変更です。エイリアス属性メソッドの生成をdefine_attribute_methodsに委譲する設計となり、aliases_by_attribute_nameという永続的なHashストレージにエイリアス名を追記していく仕組みが導入されました。
これら2つが組み合わさると、self.table_name = ... の再代入のたびにスキーマがリロードされ、alias_attribute :id_value, :id が再び呼び出されます。重複チェックのない追記により、aliases_by_attribute_name["id"] はN回のtable_name変更後にN個の"id_value"を保持します。define_attribute_methodsはその重複を含むすべてのエントリを走査してメソッドを生成するため、1クエリあたりのコストがO(N)となり、N回の操作の合計でO(N²)の劣化が生じます。PRの説明によれば、代表的な再現ケースにおいて約1000回のtable_nameスワップにかけてクエリごとのコストが単調増加し、アロケーション数も線形に増え続けることが確認されています。
Rails 7.0以前は自動エイリアスも永続ストレージも存在しなかったため、この問題は発生しませんでした。
技術的な変更
activemodel/lib/active_model/attribute_methods.rb の alias_attribute メソッド内、1行のみの変更で問題を解決しています。
変更前:
aliases_by_attribute_name[old_name] << new_name
変更後:
aliases_by_attribute_name[old_name] |= [new_name]
|=(和集合代入演算子)を使うことで、new_nameがまだ含まれていない場合のみ追加し、既存エントリとの重複を排除します。これはunless aliases.include?(new_name)による条件分岐と等価ですが、よりRubyらしいイディオムです。
メソッド内の他の処理は変更されていません。attribute_aliases.merge(new_name => old_name) はHashのmergeなので元から冪等であり、eagerly_generate_alias_attribute_methods は内部で instance_method_already_implemented? による早期リターンが実装済みのため、複数回呼ばれても安全です。
テストは2つ追加されています。ActiveModelの単体テストでは alias_attribute :bar, :foo を同一クラスに3回呼び出し、aliases_by_attribute_name["foo"] == ["bar"] となることを確認します。ActiveRecordの統合テストでは table_name を topics → authors → topics と変更しながらfirstを呼び出し、aliases_by_attribute_name["id"] == ["id_value"] が保たれることを検証します。
設計判断
ストレージ構造を変えず、書き込みを冪等にするアプローチが採用されました。
代替案としては、define_attribute_methods 側で重複をスキップするガードを追加する方法も考えられますが、それでは蓄積し続けるストレージの問題が残ります。今回の修正は根本原因である「追記時の重複」を潰すことで、下流のあらゆる処理(メソッド生成、イテレーション)が余分な作業を行わないことを保証します。
変更箇所はエイリアスを登録する単一の箇所に限定されており、影響範囲が最小化されています。|= による集合演算はRubyのArrayが標準で持つ機能であり、外部依存や新たなデータ構造を導入しない点も保守性の観点から妥当な判断といえます。
まとめ
本PRは、Rails 7.1で独立して導入された2つの変更の相互作用が引き起こす重大なリグレッションを、1行の修正で解消しています。<< を |= に置き換えるだけで、alias_attribute の呼び出しを冪等にし、table_name を頻繁に切り替えるパターンでのO(N²)コスト増大を根本から防ぐことができます。