reset_column_information で memo 化フラグをリセットして STI の不整合を防止
finder_needs_type_condition? がメモ化する @finder_needs_type_condition が、継承カラムの変更後に reset_column_information が呼ばれてもクリアされないため、STI の条件付加が正しく行われずデータ破損または例外が発生する問題を解消します。
背景
finder_needs_type_condition? は STI の type 条件を追加すべきかを判定し、結果を @finder_needs_type_condition に保持します。しかしこのフラグはサブクラスが生成されたとき(.inherited)だけリセットされ、reset_column_information/reload_schema_from_cache ではクリアされません。(総論)
この欠如により、次の二つの破壊パターンが再現可能です。(各論)1) type カラムを追加した後にスキーマ情報をリセットすると、フラグは依然として false のまま残り、STI が無視されてレコードに type が保存されません。2) 逆に type カラムを削除した後にリセットすると、フラグは true のまま残り、クエリが存在しないカラムを参照して ActiveRecord::StatementInvalid が発生します。(結論)
ModelSchema#reload_schema_from_cache は @columns_hash など多数のメモを nil にしますが、@finder_needs_type_condition だけは対象外でした。(総論)
この不整合は、スキーマ変更を伴うマイグレーションやテスト環境で reset_column_information が頻繁に呼び出されるケースで顕在化し、データ整合性とアプリケーションの安定性に直接影響します。(各論)
したがって、reset_column_information 系列の呼び出しでもフラグをクリアする必要があります。(結論)
技術的な変更
Inheritance#reload_schema_from_cache の冒頭に @finder_needs_type_condition = nil を追加し、既存のメモクリアロジックにフラグリセットを組み込みました。(総論)
def reload_schema_from_cache(*) # :nodoc:
@finder_needs_type_condition = nil
if @_new_optimized
singleton_class.remove_method(:new)
@_new_optimized = false
end
super
end
この一行追加により、reset_column_information が内部で呼び出す reload_schema_from_cache と同様にフラグが初期化され、以降の呼び出しで正しい判定が再計算されます。(各論)
テストスイートに InheritanceFinderNeedsTypeConditionTest を新規追加し、type カラムを動的に追加・削除した後にスキーマリセットを行うシナリオを検証しました。(総論)
def test_inheritance_new_with_subclass_after_adding_type_column_and_resetting_schema_cache
ActiveRecord::Base.lease_connection.rename_column :companies, :type, :old_type
Company.reset_column_information
_firm = Firm.new # populate the cache
ActiveRecord::Base.lease_connection.rename_column :companies, :old_type, :type
Company.reset_column_information
firm = Firm.new
assert_equal "Firm", firm.type
end
テストは SQLite3、PostgreSQL、MySQL の全主要データベースで成功し、既存の inheritance_test、reload_models_test、base_test とも互換性が保たれます。(結論)
設計判断
フラグリセットを 既存の reload_schema_from_cache に統合した選択は、メモクリアの責務を一元化し、コードパスの増加を最小限に抑えるというトレードオフに基づきました。(総論)
代替案としては、reset_column_information 自体に個別ロジックを追加する、あるいは新しいアクセサメソッドを導入して外部から手動でクリアさせる方法が考えられますが、いずれも既存呼び出し箇所への影響範囲が広がり、後方互換性リスクが高まります。(各論)
今回の実装は「最小限の変更で既存のリセット契約に沿う」方針を維持し、他のメモと同様に安全にリセットできる点で設計的に妥当です。(結論)
まとめ
reset_column_information がトリガーするスキーマリセット時に @finder_needs_type_condition をクリアしなかったことが、STI の条件付加に関するデータ破損と例外を招いていました。PR #57605 で Inheritance#reload_schema_from_cache にフラグリセットを追加し、テストで動的スキーマ変更に対する正しい動作を保証したことで、Rails の STI 挙動はスキーマリセットに対しても一貫性を保つようになりました。