本ガイドでは、Railsアプリケーションをclassic
モードからzeitwerk
モードに移行する方法について解説します。
このガイドの内容:
classic
モードとzeitwerk
モードについてclassic
からzeitwerk
に切り替える理由zeitwerk
モードを有効にするzeitwerk
モードで動いていることを検証するclassic
モードとzeitwerk
モードについてRailsは最初期からRails 5まで、Active Supportで実装されたオートローダーを用いていました。このオートローダーはclassic
と呼ばれ、Rails 6.xでは引き続き利用可能です。classic
オートローダーはRails 7で廃止されました。
Rails 6から、より優れた新しいオートロード方法がRailsに搭載されました。これはZeitwerkというgemに一任されています。これがzeitwerk
モードです。デフォルトでは、Railsフレームワーク6.0および6.1の読み込みはzeitwerk
モードで実行され、Rails 7で利用できるのはzeitwerk
モードのみとなります。
classic
からzeitwerk
に切り替える理由classic
オートローダーは非常に便利でしたが、取り扱いに少々注意を要したり時に混乱を招いたりする問題が多数存在していました。Zeitwerkはこうした問題を解決するために開発されました(その他にもさまざまな動機があります)。
classic
モードは非推奨化されたので、Railsを6.xにアップグレードする際にzeitwerk
モードに移行することを強く推奨します。
この移行はRails 7で完了し、classic
モードが含まれなくなりました。
大丈夫です。
Zeitwerkは従来のオートローダーとの互換性をできるだけ維持するように設計されています。現在のアプリケーションでオートロードが正しく行われていれば、切り替えは簡単です。大小さまざまなプロジェクトで、スムーズに切り替えられたことが報告されています。
本ガイドを読めば、安心してオートローダーを切り替えられます。
何らかの理由で解決方法が見当たらない状況に直面した場合は、お気軽にrails/rails
リポジトリのissueをオープンして、@fxn
にメンションしてください。
zeitwerk
モードを有効にするRails 6.0より前のバージョンを実行するアプリケーションではzeitwerk
モードを利用できません。Rails 6.0以上が必要です。
Rails 6.xアプリケーションの場合は以下の2とおりのシナリオがあります。
アプリケーションがRails 6.0または6.1のフレームワークのデフォルトを読み込んでいて、かつclassic
モードで実行されている場合は、classic
モードを手動でオプトアウトしなければなりません。これは以下のような形で行う必要があります。
# config/application.rb config.load_defaults 6.0 config.autoloader = :classic # この行を削除する
上のコメントにあるように、このオーバーライドを削除するとzeitwerk
モードがデフォルトになります。
一方、アプリケーションが古いフレームワークのデフォルトを読み込んでいる場合は、以下のようにzeitwerk
モードを明示的に有効にする必要があります。
# config/application.rb config.load_defaults 5.2 config.autoloader = :zeitwerk
Rails 7にはzeitwerk
モードしかないので、このモードを有効にするために設定を変更する必要はありません。
Rails 7ではconfig.autoloader=
セッターそのものがなくなりました。config/application.rb
にこの記述がある場合は、その行を削除してください。
zeitwerk
モードで動いていることを検証するアプリケーションがzeitwerk
モードで動いていることを検証するには、以下を実行します。
$ bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'
true
が出力されれば、zeitwerk
モードが有効です。
Zeitwerkに準拠しているかどうかのテストは、eager loadingされたファイルに対してのみ実行されます。そのため、Zeitwerkへの準拠を検証するには、すべての自動読み込みパスをeager loadパスに追加することが推奨されます。
これは既にデフォルトで行われるようになっていますが、自分のプロジェクトで自動読み込みパスを以下のようにカスタマイズしている場合は、eager loadingされないため検証されません。
config.autoload_paths << "#{Rails.root}/extras"
以下のようにすることで、手軽にeager loadingパスに追加できます。
config.autoload_paths << "#{Rails.root}/extras" config.eager_load_paths << "#{Rails.root}/extras"
zeitwerk
モードを有効にし、eager loading設定をダブルチェックしたら、以下を実行します。
$ bin/rails zeitwerk:check
チェックが成功すると以下のように出力されます。
$ bin/rails zeitwerk:check Hold on, I am eager loading the application. All is good!
アプリケーションの設定によってはこの他にも出力されることがありますが、末尾に"All is good!"があればOKです。
前節で説明したダブルチェックで、カスタムの自動読み込みパスがeager loadingパスの外にあると判断されると、タスクはそれを検出して警告を発します。しかし、テストスイートがそれらのファイルの読み込みに成功していれば、問題ありません。
Zeitwerkで期待される定数が定義されていないファイルがあると、上のタスクで通知されます。このタスクは1ファイルごとに実行されます。問題が生じたときにタスクが先に進むと、あるファイルの読み込み失敗が他の無関係な失敗に連鎖してエラー出力が読みにくくなるためです。
定数に1つでも問題があれば、その問題を解決し、"All is good!"が出力されるまでタスクを再実行してください。
たとえば以下のように出力されたとします。
$ bin/rails zeitwerk:check Hold on, I am eager loading the application. expected file app/models/vat.rb to define constant Vat
VATはヨーロッパの税制のことです。app/models/vat.rb
ではVAT
が定義済みですが、オートローダーはVat
を期待しています。どんな理由でこうなったのでしょうか。
これはZeitwerkで最もありがちな問題で、略語が関係しています。このエラーメッセージが生じた理由を考えてみましょう。
classic
オートローダーは、すべて大文字のVAT
をオートロードできます。その理由は、オートローダーの入力にconst_missing
の定数名が使われるからです。VAT
という定数に対してunderscore
が呼び出されてvat
が生成され、これを元にvat.rb
というファイルを検索することで、ファイルが正常に見つかります。
新しいZeitwerkオートローダーの入力はファイルシステムです。vat.rb
というファイルがあると、Zeitwerkはvat
に対してcamelize
を呼び出し、冒頭のみが大文字のVat
が生成されます。これにより、Vat
という定数名が定義されていることが期待されます。以上がエラーメッセージの内容です。
これは、以下のようにActiveSupport::Inflector
の語尾活用機能を用いて略語を指定するだけで簡単に修正できます。
# config/initializers/inflections.rb ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "VAT" end
上の方法は、Active Supportの語尾活用機能をグローバルに変更します。これで問題ない場合もありますが、オートローダーで用いられる語尾活用機能にオーバーライドを渡したい場合は、以下のようにします。
# config/initializers/zeitwerk.rb Rails.autoloaders.main.inflector.inflect("vat" => "VAT")
このオプションを使うと、vat.rb
というファイル名、またはvat
というディレクトリ名のみがVAT
として認識されるので、より細かく制御できます。vat_rules.rb
という名前のファイルはその影響を受けないので、VatRules
を正しく定義できるようになります。このような名前の不一致があるプロジェクトで役に立つでしょう。
以上が終われば、チェックはパスします。
$ bin/rails zeitwerk:check Hold on, I am eager loading the application. All is good!
すべて問題なく動くようになったら、テストスイートで今後もプロジェクトを検証し続けることをおすすめします。これについて詳しくはZeitwerk準拠をテストスイートでチェックするで後述します。
以下のように、concerns
サブディレクトリを持つ標準的な構造からのオートロードやeager loadingを行えます。
app/models app/models/concerns
app/models/concerns
ディレクトリはデフォルトではオートロードのパスに属しているので、これがrootディレクトリと見なされます。そのため、デフォルトではapp/models/concerns/foo.rb
ファイルで定義されるのはConcerns::Foo
ではなくFoo
になります。
アプリケーションでConcerns
が名前空間として使われている場合は、以下の2つの方法があります。
Concerns
名前空間を削除してクライアントコードを更新する。app/models/concerns
を除外することで現状のままにする。# config/initializers/zeitwerk.rb ActiveSupport::Dependencies. autoload_paths. delete("#{Rails.root}/app/models/concerns")
app
を追加するプロジェクトによっては、たとえばAPI::Base
を定義するapp/api/base.rb
を置き、オートロードパスにapp
を追加することで利用したい場合もあります。
Railsは自動的にapp
のすべてのサブディレクトリ(アセットのディレクトリなどは除く)もオートロードパスに追加するので、app/models/concerns
で起きたのと似たようなネステッドrootディレクトリの問題がここでも起きます。今後、この設定はこのままでは機能しません。
ただし、以下のようにイニシャライザでオートロードパスからapp/api
を削除すれば現状の構造を維持できます。
# config/initializers/zeitwerk.rb ActiveSupport::Dependencies. autoload_paths. delete("#{Rails.root}/app/api")
オートロードまたはeager loadingするファイルが存在しないサブディレクトリについては注意が必要です。たとえば、ActiveAdminで使うリソースを保存するapp/admin
ディレクトリがアプリケーションにある場合、以下のようにそれらのリソースを無視する必要があります。assets
ディレクトリなどについても同様の注意が必要です。
# config/initializers/zeitwerk.rb Rails.autoloaders.main.ignore( "app/admin", "app/assets", "app/javascripts", "app/views" )
上の設定がないと、アプリケーションがこれらのツリーをeager loadingします。読み込まれたファイルは定数を定義しないので、app/admin
でエラーが発生し、さらに副作用として不要なView
モジュールも生成されてしまいます。
このように、オートロードのパスにapp
ディレクトリを含めることは一応可能ですが、ややトリッキーになります。
ファイル内で、たとえば以下のようにHotel
という名前空間が定義されているとします。
app/models/hotel.rb # Hotelを定義する app/models/hotel/pricing.rb # Hotel::Pricingを定義する
このHotel
定数は、以下のようにclass
またはmodule
キーワードで設定しなければなりません。
class Hotel end
上は問題ありません。
ただし、以下の2つは無効です。
Hotel = Class.new
または
Hotel = Struct.new
これらはHotel::Pricing
などの子オブジェクトを探索できません。
これらの制約は、明示的な名前空間にのみ適用されます。名前空間を定義しないクラスやモジュールであれば上述の記法でも定義できます。
classic
モードでは、以下のように同一トップレベルに複数の定数を定義して、それらすべてを再読み込みすることも技術的に可能でした。以下のコード例で考えます。
# app/models/foo.rb class Foo end class Bar end
上のBar
は本来オートロードできないにもかかわらず、Foo
をオートロードするとBar
もオートロード済みとマーキングされました。
これはzeitwerk
モードでは利用できません。Bar
は専用のbar.rb
ファイルに移動する必要があります。これが「1ファイルにつきトップレベル定数は1個」という規則です。
これが影響するのは、上のコード例のように同一トップレベルに置かれた定数だけです。以下のようなネストしたクラスやモジュールには影響しません。
# app/models/foo.rb class Foo class InnerClass end end
アプリケーションがFoo
を再読み込みすると、Foo::InnerClass
も再読み込みされます。
config.autoload_paths
について以下のようにワイルドカードを含む設定では注意が必要です。
config.autoload_paths += Dir["#{config.root}/extras/**/"]
config.autoload_paths
のどの要素もトップレベルの名前空間(Object
)を表さなければならないので、上の設定は無効です。
これは、以下のようにワイルドカードを削除するだけで修正できます。
config.autoload_paths << "#{config.root}/extras"
アプリケーションがエンジンからクラスやモジュールをdecorateしているのであれば、どこかで以下のようなことをやっている可能性があります。
config.to_prepare do Dir.glob("#{Rails.root}/app/overrides/**/*_override.rb").sort.each do |override| require_dependency override end end
これは更新しなければなりません。以下のようにmain
オートローダーでオーバーライドを含むディレクトリを無視するように指示し、代わりにload
でそれらを読み込む必要があります。
overrides = "#{Rails.root}/app/overrides" Rails.autoloaders.main.ignore(overrides) config.to_prepare do Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override| load override end end
before_remove_const
Rails 3.1ではbefore_remove_const
というコールバックがサポートされ、このメソッドに応答するクラスやモジュールが再読み込みされるときに呼び出されるようになりました。このコールバックはドキュメント化されていないため、おそらくアプリケーションコードで使われていることはないでしょう。
しかしこのコールバックを利用している場合は、以下のように書き換えられます。
class Country < ActiveRecord::Base def self.before_remove_const expire_redis_cache end end
上を以下のように書き換えます。
# config/initializers/country.rb unless Rails.application.config.cache_classes Rails.autoloaders.main.on_unload("Country") do |klass, _abspath| klass.expire_redis_cache end end
test
環境spring gemは、アプリケーションコードが変更されると再読み込みします。test
環境でこの再読み込みを有効にするには、以下の設定が必要です。
# config/environments/test.rb config.cache_classes = false
そうしないと、以下のエラーが発生します。
reloading is disabled because config.cache_classes is true
なお、この設定でパフォーマンスは低下しません。
少なくともbootsnap 1.4.4以上に依存する必要があります。
Zeitwerkに移行するときは、zeitwerk:check
タスクを使うと便利です。プロジェクトがZeitwerkに準拠したら、このチェックを自動化することをおすすめします。これを行うには、アプリケーションをeager loadingするだけで十分です(実際zeitwerk:check
が行うのはそれだけです)。
プロジェクトにCI(Continuous Integration: 継続的インテグレーション)環境がある場合は、テストスイートを実行するときにアプリケーションをeager loadingするとよいでしょう。アプリケーションが何らかの理由でeager loadingできなくなっていることにproduction環境で気づくより、CI環境で知りたいものですよね。
CIでは、テストスイートが実行中であることを示すのに何らかの環境変数を設定することがよくあります。環境変数がCI
の場合は、以下のように指定できます。
# config/environments/test.rb config.eager_load = ENV["CI"].present?
Rails 7以降で新規生成したアプリケーションでは、デフォルトで上の設定が有効になります。
プロジェクトにCI環境がない場合は、テストスイートでRails.application.eager_load!
を呼ぶことでeager loadingできます。
require "test_helper" class ZeitwerkComplianceTest < ActiveSupport::TestCase test "eager loads all files without errors" do assert_nothing_raised { Rails.application.eager_load! } end end
require "rails_helper" RSpec.describe "Zeitwerk compliance" do it "eager loads all files without errors" do expect { Rails.application.eager_load! }.not_to raise_error end end
require
呼び出しはすべて削除すること私の経験では、プロジェクトでのrequire
呼び出しは一般に不要です。しかしrequire
を呼び出しているプロジェクトをいくつか実際に見かけたことがあり、他にもそうした噂をいくつか耳にしています。
Railsアプリケーションでは、require
は「lib
のコード」「gemなどのサードパーティ依存関係」「標準ライブラリ」の読み込みにしか使いません。アプリケーションのオートロード可能なコードは決してrequire
しないでください。この方法がよくない理由については、Classicモードの解説で説明しました。
require "nokogiri" # よい require "net/http" # よい require "user" # 絶対ダメ、削除せよ(app/models/user.rbがある場合)
そのようなrequire
呼び出しはすべて削除してください。
require_dependency
呼び出しの削除Zeitwerkによって、require_dependency
の既知のユースケースはすべて削除されました。プロジェクトをgrepしてrequire_dependency
をすべて削除してください。
アプリケーションでSTIを利用している場合は、『定数の自動読み込みと再読み込み(Zeitwerk)』ガイドの『STI(単一テーブル継承)』を参照してください。
クラスやモジュールの定義で、以下のような定数パスを安定して利用できるようになりました。
# このクラスの本体でのオートロードがRubyのセマンティクスと一致するようになった class Admin::UsersController < ApplicationController # ... end
1つ注意すべき点があります。実行順序によってはclassicオートローダーで以下のFoo::Wadus
をオートロードできてしまう場合がありました。
class Foo::Bar Wadus end
このFoo
はネストの中に存在しないので、上はRubyのセマンティクスと一致しません。そのため、これはzeitwerk
モードではまったく動作しません。このようなエッジケースに遭遇した場合は、以下のようにFoo::Wadus
という修飾名を利用できます。
class Foo::Bar Foo::Wadus end
あるいは、以下のようにFoo
をネストに追加します。
module Foo class Bar Wadus end end
RailsにはWebリクエストをスレッドセーフにするロックが用意されているにもかかわらず、classic
モードの定数自動読み込みはスレッドセーフではありませんでした。
zeitwerk
モードの定数自動読み込みはスレッドセーフです。たとえば、runner
コマンドで実行されるマルチスレッドのスクリプトをオートロードできるようになりました。
classic
モードでは、app/models/foo.rb
ファイルでBar
が定義されていると、このファイルをオートロードできません。しかしclassic
モードはファイルの読み込みを機械的に再帰するので、このファイルのeager loadingは可能です。テストを最初にeager loadingする形で実行すると、その後オートロードが発生したときに実行が失敗する可能性があります。
zeitwerk
モードではどちらの読み込みモードも一貫しているので、テストの失敗やエラーは同じファイル内で発生します。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。