本ガイドでは、Railsアプリケーションをclassicモードからzeitwerkモードに移行する方法について解説します。
このガイドの内容
classicモードとzeitwerkモードについてclassicからzeitwerkに切り替える理由zeitwerkモードを有効にするzeitwerkモードで動いていることを検証するclassicモードとzeitwerkモードについてRailsは最初期からRails 5まで、Active Supportで実装されたオートローダーを用いていました。このオートローダーはclassicと呼ばれ、Rails 6.xでは引き続き利用可能です。Rails 7ではclassicオートローダーが含まれなくなりました。
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が出力されれば、zeitwekモードが有効です。
zeitwerkモードを有効にしてから、以下を実行します。
bin/rails zeitwerk:check
チェックが成功すると以下のように出力されます。
% bin/rails zeitwerk:check Hold on, I am eager loading the application. All is good!
アプリケーションの設定によってはこの他にも出力されることがありますが、末尾に"All is good!"があればOKです。
Zeitwerkで期待される定数が定義されていないファイルがあると、上のタスクで通知されます。このタスクは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.each do |autoloader| autoloader.inflector.inflect("vat" => "VAT") end
以上を反映すればチェックはパスします。
% bin/rails zeitwerk:check Hold on, I am eager loading the application. All is good!
以下のように、concernsサブディレクトリを持つ標準的な構造からのオートロードやeager loadingを行えます【チェック】。
app/models app/models/concerns
app/models/concernsディレクトリはデフォルトではオートロードのパスに属しているので、これがルートディレクトリと見なされます。そのため、デフォルトでは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")
ファイル内で、たとえば以下のようにHotelという名前空間が定義されているとします。
app/models/hotel.rb # Hotelを定義する app/models/hotel/pricing.rb # Hotel::Pricingを定義する
このHotel定数は、以下のようにclassまたはmoduleキーワードで設定しなければなりません。
class Hotel end
上は問題ありません。
ただし、以下は無効です。
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"
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以上に依存するようにしてください。
Rakeタスクzeitwerk:checkは、単にeager loadingを実行します。これによってZeitwerkの組み込みバリデーションがトリガされます。
これと同等のタスクをテストスイートに追加すれば、テストカバレッジに関係なくアプリケーションの読み込みが常に正しく行われるようになります。
require "test_helper" class ZeitwerkComplianceTest < ActiveSupport::TestCase test "eager loads all files without errors" do Rails.application.eager_load! rescue => e flunk(e.message) else pass 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_dependency呼び出しの削除Zeitwerkによって、require_dependencyの既知のユースケースはすべて削除されました。プロジェクトをgrepしてrequire_dependencyをすべて削除してください。
アプリケーションでSTIを利用している場合は、『定数の自動読み込みと再読み込み (Zeitwerk)ガイド』の『STI(単一テーブル継承)』を参照してください。
クラスやモジュールの定義で、以下のような定数パスを安定して利用できるようになりました。
# このクラスの本体でのオートロードがRubyのセマンティクスと一致するようになった class Admin::UsersController < ApplicationController # ... end
ひとつ注意すべきは、実行順序によっては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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。