Active Record と Active Model の不要なメモ化を除去し、Ractor 互換性を向上

rails/rails

Active Record と Active Model で保持していた複数のメモ化変数を削除し、非メイン Ractor でも安全に利用できるようにしました。ベンチマークはパフォーマンスへの影響が僅かであることを示しており、機能上の変更はありません。

背景

メモ化は属性情報や Arel テーブルインスタンスを各クラスでキャッシュするために使われてきましたが、Ractor 境界を跨ぐ際に共有できないオブジェクトが残るため、非メイン Ractor での利用が制限されていました。PR の目的は、これらのキャッシュを削除または安全な形に置き換えて、Ractor フレンドリーな設計にすることです。ベンチマークは Post#title などの典型的な操作で差が数パーセント以内であることを示し、実務上の影響は最小と評価されています。

技術的な変更

Active Model の属性メソッドキャッシュ

@attribute_method_patterns_cache をクラス変数から Concurrent::Map へ置き換え、スレッド安全なキャッシュとして再初期化ロジックを追加しました。generate_alias_attribute_methodsdefine_attribute_methodundefine_attribute_methods での参照を @attribute_method_patterns_cache に統一し、clear 呼び出しもインスタンス変数経由に変更しています。```ruby:activemodel/lib/active_model/attribute_methods.rb
@@
- @attribute_method_patterns_cache = nil
+ @attribute_method_patterns_cache = Concurrent::Map.new(initial_capacity: 4)
@@
- attribute_method_patterns_cache.clear
+ @attribute_method_patterns_cache.clear


### Active Record の属性メソッドキャッシュ

同様に **`attribute_method_patterns_cache`** のクリアをインスタンス変数 `@attribute_method_patterns_cache` に統一しました。```ruby:activerecord/lib/active_record/attribute_methods.rb
@@
-        attribute_method_patterns_cache.clear
+        @attribute_method_patterns_cache.clear

PrimaryKey のメモ化除去

primary_key= で設定されていた @attributes_builder のリセット処理と、継承時に _attributes_builder を明示的に nil にしていたコードを削除しました。これにより、属性ビルダーは必要時にのみ生成されます。

Core の Arel テーブル初期化

@arel_table をクラス定義時に Arel::Table.new(klass: self) で確定させ、遅延初期化ロジックを除去しました。arel_table メソッドは単にインスタンス変数を返すだけになり、サブクラスが継承した際にも常に有効なテーブルオブジェクトが保持されます。```ruby:activerecord/lib/active_record/core.rb
@@
- @arel_table = nil
+ @arel_table = Arel::Table.new(klass: self)
@@
- @arel_table ||= Arel::Table.new(table_name, klass: self)
+ @arel_table


### ModelSchema の列名と属性ビルダー

`column_names` のメモ化を **`@column_names`** から直接計算へ変更し、`attributes_builder` もキャッシュせず毎回新規に構築するようにしました。`reload_schema_from_cache` でリセットする箇所も同様に **`@arel_table`** の再生成に統一しました。```ruby:activerecord/lib/active_record/model_schema.rb
@@
-        @column_names ||= columns.map(&:name).freeze
+        columns.map(&:name).freeze
@@
-        @attributes_builder ||= begin
-          defaults = _default_attributes.except(*(column_names - Array(primary_key)))
-          ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
-        end
+        defaults = _default_attributes.except(*(column_names - Array(primary_key)))
+        ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
@@
-          @arel_table = nil
+          @arel_table = Arel::Table.new(klass: self)

Scoping のデフォルトスコープロジック

default_scope_override クラス属性を削除し、代わりに Base.is_a?(method(:default_scope).owner) の結果を直接判定してデフォルトスコープを呼び出すシンプルな条件分岐に置き換えました。これにより余計な状態保持がなくなり、ロジックが明快になっています。```ruby:activerecord/lib/active_record/scoping/default.rb
@@
- class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil
@@
- if default_scope_override.nil?
- self.default_scope_override = !Base.is_a?(method(:default_scope).owner)

- end

  • if default_scope_override
  • if !Base.is_a?(method(:default_scope).owner)

### Arel::Table のインタフェース改良

`Table` に **`name`** のリーダを書き換え、`initialize` で **`name` または `klass` が必須** とするバリデーションを追加しました。`hash` も `name.hash` を用いることで名前が明示的に使用され、`attr_writer :name` に合わせました。さらに `attr_accessor :name` を `attr_writer` に変更し、名前取得は `name` メソッドで `@klass` のテーブル名をフォールバックします。```ruby:activerecord/lib/active_record/arel/table.rb
@@
-    attr_accessor :name
+    attr_writer :name
@@
-    def initialize(name, as: nil, klass: nil, type_caster: klass&.type_caster)
+    def initialize(name = nil, as: nil, klass: nil, type_caster: klass&.type_caster)
+      raise ArgumentError, "A name or klass is required." unless klass || name
@@
-    def name
-      @name
-    end
+    def name
+      @name || @klass&.table_name
+    end
@@
-      @name.hash
+      name.hash

テストコードの調整

FakeRecord::Basetable_nametype_caster のスタブを追加し、Table の新しいコンストラクタと name ロジックを検証するテストを追加しました。これにより、変更後の API が期待通りに動作することが CI で保証されます。

設計判断

メモ化除去は Ractor が各スレッド間で共有できないオブジェクトを保持しないことを最優先にした設計選択です。Concurrent::Map の導入でスレッド安全なキャッシュを確保しつつ、@arel_table の即時生成によりクラス継承時の状態不整合を防ぎました。default_scope_override の削除は余計な状態保持を排除し、ロジックをシンプルに保つためのトレードオフです。全体としてパフォーマンスへの影響はベンチマークで確認された範囲内に抑えられ、Ractor 互換性が大幅に向上しました。

まとめ

本 PR は Active Record と Active Model の不要なメモ化を取り除き、Ractor 環境で安全に利用できるようにした変更です。コードベースの状態保持を削減しつつ、ベンチマークは実務上の性能低下がないことを示しています。今後の Rails 開発において Ractor への対応が容易になることが期待されます。

記事メタデータ

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

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的詳細、設計判断(任意)、まとめの全要素が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは正しいファイル名付きシンタックスハイライト形式です。PRリンクは "PR #57642" となっており、要求される "[#57642](URL)" 形式になっていません。また、コミットIDのリンクが記載されていません。

対象読者への適合性 ✓ PASS

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

専門的なエンジニア向けに記述されており、初心者向けの過度な説明はありません。

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

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

各セクションは総論パラグラフ、各論パラグラフ、結論パラグラフで構成され、段落はトピックセンテンスで始まり、1段落1トピック・適切な長さが保たれています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと一致しており、追加・削除・変更が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語はPR情報と合致しており、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの背景・目的・ベンチマーク結果と整合しており、誤りや根拠のない主張はありません。

事実の突合 ✓ PASS

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

全ての主張はPRの記載内容に基づいており、外部知識や推測は含まれていません。

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

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

PR番号 #57642 が正しく記載されており、具体的な数値の誤りはありません。

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

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

記事タイトルはPRの目的(不要なメモ化除去)を正確に表現しています。

外部知識の正確性 ✓ PASS

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

LTSやリリース日程等、PRに記載のない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

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