はじめに
Rails の load_defaults を追従している際、new_framework_defaults_x_x.rb に記載されている設定をすべて有効化したため、config/application.rb の load_defaults を対象バージョンにした。そのタイミングにテストで失敗したことがある。
調査したところ、有効化したはずの設定で発生したエラーだった。new_framework_defaults_x_x.rb で有効化したはずなのに、なぜ config/application.rb の load_defaults を設定したタイミングで現れたのか…!
Railsの new_framework_defaults_x_x.rb にある設定を有効化しても反映されないものがあることについて、調べた内容をまとめる。
new_framework_defaults_x_x.rb の役割
Rails では config/application.rb に config.load_defaults X.X を記載することで、そのバージョンのフレームワークデフォルト値をまとめて有効化できます。
アップグレード時には rails app:update コマンドで config/initializers/new_framework_defaults_x_x.rb が生成されます。このファイルでは対象バージョンで変更される設定がコメントアウト状態で列挙されており、コメントを外すことで各設定を一つずつ確認しながら有効化できます。
new_framework_defaults_x_x.rb は config/initializers/ 以下に配置されるため、Rails の通常の initializer として処理されます。
何が起きているのか
例えば、このような流れがあったとする。
-
new_framework_defaults_7_0.rb内の設定をすべてコメントアウトを外して有効化した - テストを実行し、すべて通ることを確認した
-
config/application.rbのload_defaultsを7.0に変更した - テストが失敗した
ステップ1ですでに有効化した設定のはずなのに、なぜステップ3のタイミングで初めてエラーが出るのだろうか。new_framework_defaults_7_0.rb での有効化がそもそも機能していなかった、ということになるのだろうか。
原因
原因は、ActiveSupport.on_load と gem による早期ロードによって発生しているようです。
active_record.set_configs initializer の仕組みとして、Rails は active_record.set_configs という Railtie initializer の中で ActiveSupport.on_load(:active_record) を使って設定を適用する。
# Rails 内部の処理(概略)
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
app.config.active_record.each do |k, v|
send("#{k}=", v)
end
end
end
ActiveSupport.on_load(:active_record) のコールバックは、ActiveRecord::Base が読み込まれたタイミングで実行される。具体的には ActiveRecord::Base クラス定義の末尾にある run_load_hooks(:active_record, Base) の呼び出しで発火する。
問題が起きるのは、gem が ActiveRecord::Base を config/initializers/ の実行前に読み込む場合です。
load_defaults 6.1 のままの場合(設定が効かない):
-
config/application.rbの評価-
load_defaults 6.1→ 6.1 のデフォルト値が設定される
-
- gem が
ActiveRecord::Baseを早期ロード(eager require)する-
run_load_hooks(:active_record, Base)が発火 -
active_record.set_configsのon_loadコールバックが実行される
→ このとき設定されている値は 6.1 のデフォルト値
-
-
config/initializers/new_framework_defaults_7_0.rbが実行される-
config.active_record.some_setting = trueを設定する
→on_loadコールバックはすでにステップ 2 で実行済みのため、無視される
-
load_defaults 7.0 を config/application.rb に書いた場合(正しく反映される):
-
config/application.rbの評価-
load_defaults 7.0→ 7.0 のデフォルト値が設定される ← ここで有効化される
-
- gem が
ActiveRecord::Baseを早期ロードする-
on_loadコールバックが実行される
→ このとき設定されている値はすでに 7.0 のデフォルト値 ← 正しく反映される
-
これが「new_framework_defaults_x_x.rb で有効化したはずなのに、load_defaults を上げたタイミングで初めてエラーが出た」理由とのことでした。
Rails に起票された Issue での見解
この問題は Rails の Issue(#31285、2017年)としても報告されていた。回答としては「gem側が ActiveSupport.on_load を使わずに ActiveRecord::Base を参照しているバグ」と判断してクローズしている。gem 作者の実装次第で発生するため、Rails本体での修正は行われていない。
該当する設定の例
参考記事によると、Rails 7.1 へのアップグレードでこの問題が報告されている設定には以下のようなものが含まれています。
active_record.default_column_serializeractive_record.raise_on_assign_to_attr_readonlyactive_record.run_commit_callbacks_on_first_saved_instances_in_transactionactive_record.encryption.hash_digest_classaction_controller.allow_deprecated_parameters_hash_equality
対応策
1. 効かない設定は config/application.rb に書く
早期ロードの影響を受ける設定は config/application.rb の load_defaults の直後に記述する。
# config/application.rb
module MyApp
class Application < Rails::Application
config.load_defaults 6.1
# new_framework_defaults_7_0.rb に書いても on_load コールバックより後に
# 評価されるため効かない設定はここに書く
config.active_record.partial_inserts = false
config.active_record.automatic_scope_inversing = true
end
end
load_defaults のバージョンを上げた後、この記述は冗長になるが、アップグレード前の段階では正しく有効化するためにここへ書く必要があります。
2. ファイルをリネームして他の initializer より先に実行する
Issue #31285 のコメントでは、new_framework_defaults_x_x.rb を 0_new_framework_defaults_x_x.rb にリネームすることで、他の config/initializers/ 内の initializer より先に実行させるという回避策も提案されています。
ただし、これが有効なのは他の initializer 内で ActiveRecord::Base が読み込まれているケースに限られます。gem の require 時点(initializer より前)で早期ロードが発生している場合には効果がないです。
おわりに
早期ロードによって、こういった事象が発生することを学んだ。もし同じ事象に遭遇した方の参考になればと思います。