本書ではZeitwerk
モードによって行われる自動読み込み(autoloading: オートロード)と、再読み込み(reloading: リロード)の仕組みについて説明します。
このガイドの内容:
本ガイドでは、Railsアプリケーションの「自動読み込み」「再読み込み」「eager loading」について解説します
通常のRubyプログラムでは、使いたいクラスやモジュールを定義したファイルを明示的に読み込みます。たとえば、以下のコントローラではApplicationController
クラスやPost
クラスを参照しており、通常はこれらに対してrequire
呼び出しを行います。
# Railsではこのように書かないこと require "application_controller" require "post" # Railsではこのように書かないこと class PostsController < ApplicationController def index @posts = Post.all end end
Railsアプリケーションでは上のようなことはしません。アプリケーションのクラスやモジュールは、require
呼び出しを行なわずに、どこでも利用できます。
class PostsController < ApplicationController def index @posts = Post.all end end
Railsは、必要に応じてクラスやモジュールを開発者の代わりに自動読み込み(autoload)します。これが可能になるのは、RailsがセットアップするいくつかのZeitwerkローダーのおかげです。これらのローダーは、自動読み込み、再読み込み、eager loadingを提供します。
ただし、これらのローダーはそれ以外のものを一切管理しません。特に、Ruby標準ライブラリやgemの依存関係、Railsコンポーネント自体、さらには(デフォルトで)アプリケーションのlib
ディレクトリさえも管理しません。これらのコードは通常どおりに読み込む必要があります。
Railsアプリケーションで使うファイル名は、そこで定義されている定数名と一致しなければなりません。ファイル名はディレクトリ名と合わせて名前空間として振る舞います。
たとえば、app/helpers/users_helper.rb
ファイルではUsersHelper
を定義すべきですし、app/controllers/admin/payments_controller.rb
ではAdmin::PaymentsController
を定義すべきです。
デフォルトのRailsは、ファイル名をString#camelize
メソッドで活用するようZeitwerkを設定します。たとえば、"users_controller".camelize
はUsersController
を返すので、app/controllers/users_controller.rb
ではUsersController
という定数が定義されることが期待されます。
このような活用形をカスタマイズする方法については、本ガイドの「活用形をカスタマイズする」で後述します。
詳しくはZeitwerkのドキュメントを参照してください。
config.autoload_paths
自動読み込みパス(autoload path: オートロードパス)とは、その中身が自動読み込みの対象となるアプリケーションディレクトリ(app/models
など)のリストを指します。これらのディレクトリはルート名前空間であるObject
を表します。
Zeitwerkのドキュメントでは自動読み込みのパスをルートディレクトリと呼んでいますが、本ガイドでは「自動読み込みパス」と呼びます。
自動読み込みパスの下にあるファイル名は、Zeitwerkのドキュメントに記載されているとおりに定義された定数と一致しなければなりません。
デフォルトでは、あるアプリケーションの自動読み込みパスは次のもので構成されています。アプリケーションの起動時にapp
の下にあるすべてのサブディレクトリ(assets
、javascript
、views
は除く)と、アプリケーションが依存する可能性のあるエンジンの自動読み込みパスです。
たとえば、app/helpers/users_helper.rb
にUsersHelper
が実装されていれば、そのモジュールは以下のように自動読み込み可能になります。したがってrequire
呼び出しは不要です(し、書くべきではありません)。
$ rails runner 'p UsersHelper' UsersHelper
Railsの自動読み込みパスには、app
の下のあらゆるカスタムディレクトリも自動的に追加されます。たとえば、アプリケーションにapp/presenters
ディレクトリがあれば、自動読み込みの設定を変更しなくてもapp/presenters
の下に置かれたものをすぐ利用できます。
デフォルトの自動読み込みパスの配列は、config/application.rb
またはconfig/environments/*.rb
で以下のようにconfig.autoload_paths
に追加することで拡張可能です。
module MyApplication class Application < Rails::Application config.autoload_paths << "#{root}/extras" end end
また、Railsエンジンはエンジンクラスの本文内やエンジン独自のconfig/environments/*.rb
にあるものを自動読み込みパスに追加することも可能です。
ActiveSupport::Dependencies.autoload_paths
はくれぐれも改変しないでください。自動読み込みパスを変更するpublicなインターフェイスはconfig.autoload_paths
の方です。
アプリケーションの起動中は、自動読み込みパス内のコードは自動で読み込まれません(特にconfig/initializers/*.rb
の中)。正しい方法については、後述のアプリケーション起動時の自動読み込みを参照してください。
自動読み込みパスは、Rails.autoloaders.main
オートローダーによって管理されます。
config.autoload_lib(ignore:)
デフォルトでは、lib
ディレクトリはアプリケーションやエンジンの自動読み込みパスに含まれません。
設定メソッドconfig.autoload_lib
は、lib
ディレクトリをconfig.autoload_paths
とconfig.eager_load_paths
に追加します。このメソッドはconfig/application.rb
またはconfig/environments/*.rb
から呼び出される必要があり、エンジンでは利用できません。
通常、lib
ディレクトリには、オートローダーによって管理されるべきでないサブディレクトリが含まれています。そのため、たとえば以下のようにignore
キーワード引数にlib
からの相対パスで指定してください。
config.autoload_lib(ignore: %w(assets tasks))
このようにする理由は、assets
ディレクトリとtasks
ディレクトリは通常のRubyコードでlib
ディレクトリを共有していますが、それらのディレクトリの内容は自動読み込みやeager loadingのためのものではないからです。
このignore
リストには、lib
のサブディレクトリのうち、拡張子が.rb
のファイルを含まないサブディレクトリや、自動読み込みもeager loadもすべきでないサブディレクトリを以下のようにすべて含めておくべきです。
config.autoload_lib(ignore: %w(assets tasks templates generators middleware))
config.autoload_lib
はRails 7.1より前では利用できませんが、アプリケーションがZeitwerkを利用している限り、以下のように引き続きエミュレーションできます。
# config/application.rb module MyApp class Application < Rails::Application lib = root.join("lib") config.autoload_paths << lib config.eager_load_paths << lib Rails.autoloaders.main.ignore( lib.join("assets"), lib.join("tasks"), lib.join("generators") ) # ... end end
config.autoload_once_paths
クラスやモジュールを再読み込みせずに自動読み込みできるようにしたい場合があります。autoload_once_paths
には、自動読み込みするが再読み込みはしないコードの保存場所を指定します。
このコレクションはデフォルトでは空ですが、config.autoload_once_paths
に追加する形で拡張可能です。たとえば以下はconfig/application.rb
またはconfig/environments/*.rb
に書けます。
module MyApplication class Application < Rails::Application config.autoload_once_paths << "#{root}/app/serializers" end end
また、Railsエンジンはエンジンクラスの本文内やエンジン独自のconfig/environments/*.rb
にあるものを自動読み込みパスに追加することも可能です。
app/serializers
をconfig.autoload_once_paths
に追加すると、app/
の下のカスタムディレクトリであってもRailsはこれを自動読み込みパスと見なさなくなります。この設定を行うと、このルールが上書きされます。
これは、Railsフレームワーク自体のような、再読み込みが可能な場所でキャッシュされるクラスやモジュールで重要になります。
たとえば、Active JobシリアライザをActive Jobの中に保存するとします。
# config/initializers/custom_serializers.rb Rails.application.config.active_job.custom_serializers << MoneySerializer
再読み込みが発生しても、Active Jobそのものは再読み込みされず、自動読み込みパスにあるアプリケーションコードとエンジンコードのみが再読み込みされます。
このMoneySerializer
を再読み込み可能にすると、改変バージョンのコードを再読み込みしてもActive Job内に保存されるそのクラスのオブジェクトに反映されないので、混乱が生じる可能性があります。実際にMoneySerializer
を再読み込み可能にすると、Rails 7以降ではそのようなイニシャライザでNameError
が発生します。
別のユースケースは、以下のようにフレームワークのクラスをdecorateするエンジンの場合です。
initializer "decorate ActionController::Base" do ActiveSupport.on_load(:action_controller_base) do include MyDecoration end end
この場合、イニシャライザが実行される時点ではMyDecoration
に保存されているモジュールオブジェクトがActionController::Base
の先祖となるので、MyDecoration
を再読み込みしても先祖への継承チェイン(ancestor chain)に反映されず、無意味です。
autoload_once_paths
のクラスやモジュールはconfig/initializers
で自動読み込み可能です。すなわち、以下の設定を行うことで動くようになります。
# config/initializers/custom_serializers.rb Rails.application.config.active_job.custom_serializers << MoneySerializer
技術的には、once
オートローダーによって管理されるクラスやモジュールは、:bootstrap_hook
より後に実行される任意のイニシャライザで自動読み込みが可能です。
autoload_once_paths
は、Rails.autoloaders.once
で管理されます。
config.autoload_lib_once(ignore:)
config.autoload_lib_once
メソッドは、config.autoload_lib
と似ていますが、lib
をconfig.autoload_once_paths
に追加する点が異なります。このメソッドは、config/application.rb
またはconfig/environments/*.rb
から呼び出す必要があります。エンジンでは利用できません。
config.autoload_lib_once
を呼び出すことで、lib
内のクラスやモジュールが自動的に読み込まれます。アプリケーションの初期化時でも再読み込みは行われません。
config.autoload_lib_once
は7.1以前では利用できませんが、アプリケーションがZeitwerkを使用している限り、エミュレーションは可能です。
# config/application.rb module MyApp class Application < Rails::Application lib = root.join("lib") config.autoload_once_paths << lib config.eager_load_paths << lib Rails.autoloaders.once.ignore( lib.join("assets"), lib.join("tasks"), lib.join("generators") ) # ... end end
Railsアプリケーションのファイルが変更されると、クラスやモジュールを自動的に再読み込みします。
正確に言うと、Webサーバーが実行中の状態でアプリケーションのファイルが変更されると、Railsは次のリクエストが処理される直前に、main
オートローダが管理しているすべての定数をアンロードします。これによって、アプリケーションでリクエスト継続中に使われるクラスやモジュールが自動読み込みされるようになり、続いてファイルシステム上の現在の実装が反映されます。
再読み込みは有効にも無効にもできます。この振る舞いを制御するのはconfig.enable_reloading
設定です。これはdevelopment
モードではデフォルトでtrue
(再読み込みが有効)、production
モードではfalse
(再読み込みが無効)になります。
デフォルトのRailsは、変更されたファイルをイベンテッドファイルモニタで検出しますが、自動読み込みパスを調べてファイル変更を検出することも可能です。これは、config.file_watcher
の設定で制御されます。
Railsコンソールでは、config.enable_reloading
の値にかかわらずファイルウォッチャーは動作しません(通常、コンソールセッションの最中に再読み込みが行われると混乱を招く可能性があるためです)。一般にコンソールセッションは、 個別のリクエストと同様に変化しない、一貫したアプリケーションクラスとモジュールのセットによって提供されることが望まれます。
ただし、コンソールでreload!
を実行することで強制的に再読み込みできます。
irb(main):001:0> User.object_id => 70136277390120 irb(main):002:0> reload! Reloading... => true irb(main):003:0> User.object_id => 70136284426020
上のように、User
定数に保存されているクラスオブジェクトは、再読み込みすると異なるものに変わります。
Rubyには、メモリ上のクラスやモジュールを真の意味で再読み込みする手段もなければ、既に利用されているすべてのクラスやモジュールに再読み込みを反映する手段もないことを理解しておくことが、きわめて重要です。技術的には、User
クラスを「アンロード」することは、Object.send(:remove_const, "User")
でUser
定数を削除するということです。
たとえば、Railsコンソールセッションで以下をチェックしてみます。
irb> joe = User.new irb> reload! irb> alice = User.new irb> joe.class == alice.class => false
joe
は元のUser
クラスのインスタンスです。再読み込みが発生すると、このUser
定数はそれまでと異なる、再読み込みされたクラスとして評価されます。alice
は新たに読み込んだUser
クラスのインスタンスですが、joe
のクラスはそうではなく、クラスが古くなっています(stale)。この場合はreload!
を再度呼び出す代わりに、joe
を再度定義するか、IRBサブセッションを起動するか、単に新しいコンソールセッションを起動することでも解決します。
また、再読み込み可能なクラスを、再読み込みされない場所でサブクラス化している場合にもこの問題が発生する可能性があります。
# lib/vip_user.rb class VipUser < User end
User
が再読み込みされてもVipUser
は再読み込みされないので、VipUser
のスーパークラスは元の古いクラスのオブジェクトのままです。
結論: 再読み込み可能なクラスやモジュールをキャッシュしてはいけません。
起動中のアプリケーションは、once
オートローダが管理するautoload_once_paths
からの自動読み込みが可能です(詳しくは前述のconfig.autoload_once_paths
を参照)。
ただし、main
オートローダが管理している自動読み込みパスからの自動読み込みはできません。これは、config/initializers
にあるコードや、アプリケーションやエンジンのイニシャライズについても同様です。
イニシャライザーは、アプリケーションの起動時に一度だけ実行されます。再読み込み時に再度実行されることはありません。もしイニシャライザがリロード可能なクラスやモジュールを使用していた場合、それらに対する編集はその初期コードに反映されないため、古くさくなってしまいます。したがって、初期化時にリロード可能な定数を参照することは禁止されています。
その理由は、イニシャライザはアプリケーション起動時に1度しか実行されないためです。再読み込み時に再実行されることはありません。イニシャライザが再読み込み可能なクラスやモジュールを利用している場合、それらを編集しても初期コードに反映されないため、古くなってしまいます。この理由により、初期化時に再読み込み可能な定数を参照することは禁止されています。
では、代わりにどうすればいいのか見てみましょう。
main
オートローダーが管理しているapp/services
にApiGateway
という再読み込み可能なクラスがあるとします。そしてアプリケーション起動時にエンドポイントを設定する必要が生じたとします。以下のように書くとエラーになります。
# config/initializers/api_gateway_setup.rb ApiGateway.endpoint = "https://example.com" # NameError
イニシャライザは再読み込み可能な定数を参照できないので、to_prepare
ブロックを使って、再読み込み時に実行されるコードをラップする必要があります。この部分は起動時に読み込まれ、また再読み込みのたびに読み込まれるようになります。
# config/initializers/api_gateway_setup.rb Rails.application.config.to_prepare do ApiGateway.endpoint = "https://example.com" # 正しい end
歴史的な理由により、このコールバックは2回実行される可能性があります。ここで実行するコードは冪等でなければなりません。
再読み込み可能なクラスとモジュールは、after_initialize
ブロックでも自動読み込みが可能です。これらは起動時に実行されますが、再読み込み時には再実行されません。例外的な状況では、この振る舞いにしたいこともあります。
以下のようなプリフライトチェックはそうしたユースケースのひとつです。
# config/initializers/check_admin_presence.rb Rails.application.config.after_initialize do unless Role.where(name: "admin").exists? abort "adminロールが存在しません。データベースのseedを行ってください。" end end
設定によっては、何らかのクラスやモジュールのオブジェクトを受け取って、それを再読み込みされない場所に保存するものもあります。こうした設定では、コードの編集の結果がキャッシュ済みの古いオブジェクトに反映されないため、設定に渡すものは再読み込み可能にしないことが重要です。
そうした例の1つがミドルウェアです。
config.middleware.use MyApp::Middleware::Foo
再読み込みを行っても、このミドルウェアスタックには反映されないので、MyApp::Middleware::Foo
が再読み込み可能になっていると混乱の元になります(その実装を変更しても何も反映されません)。
別の例は、Active Jobシリアライザです。
# config/initializers/custom_serializers.rb Rails.application.config.active_job.custom_serializers << MoneySerializer
初期化時にMoneySerializer
が評価したものは、すべてこのカスタムシリアライザにプッシュされ、そのオブジェクトは再読み込み時にも残り続けます。
さらに別の例は、Railtieやエンジンがモジュールをinclude
してフレームワークのクラスをdecorateする場合です。たとえば、turbo-rails
はActiveRecord::Base
を以下のようにdecorateします。
initializer "turbo.broadcastable" do ActiveSupport.on_load(:active_record) do include Turbo::Broadcastable end end
これにより、ActiveRecord::Base
の先祖への継承チェインにモジュールオブジェクトが追加されます。しかし再読み込みが発生してもTurbo::Broadcastable
の変更は反映されないので、先祖への継承チェインには元のオブジェクトが引き続き残ります。
すなわち、こうしたクラスやモジュールは再読み込み可能にできません。
Railsの規約に沿った形で編成するには、これらのファイルをlib/
ディレクトリに配置してから、必要に応じてrequire
で読み込みます。たとえば、アプリケーションにlib/middleware
がある場合は、それを設定する前に通常のrequire
呼び出しを行います。
require "middleware/my_middleware" config.middleware.use MyMiddleware
また、lib/
がオートロードパスに含まれている場合は、以下のようにオートローダーがlib/
のサブディレクトリを無視するように設定してください。
# config/application.rb config.autoload_lib(ignore: %w(assets tasks ... middleware))
理由は、これらのファイルは自分で読み込まれるためです。
別の方法は、上述のように、それらをautoload_once_paths
ディレクトリで定義して自動読み込みすることです(詳しくは前述のconfig.autoload_once_paths
を参照)。
あるエンジンが、ユーザーをモデリングする再読み込み可能なアプリケーションクラスと連携する場合、そのための設定ポイントが以下のようになっていると、エラーになります。
# config/initializers/my_engine.rb MyEngine.configure do |config| config.user_model = User # NameError end
再読み込み可能なアプリケーションコードをエンジンで正しく扱うには、代わりにアプリケーションのクラス名を文字列で設定する必要があります。
# config/initializers/my_engine.rb MyEngine.configure do |config| config.user_model = "User" # OK end
これで、実行時にconfig.user_model.constantize
で現在のクラスオブジェクトを得られるようになります。
一般的にproduction的な環境では、アプリケーションの起動時にアプリケーションコードをすべて読み込んでおく方が望ましいと言えます。eager loading(一括読み込み)はすべてをメモリ上に読み込むことでリクエストに即座に対応できるように備え、CoW(コピーオンライト)との相性にも優れています。
eager loadingはconfig.eager_load
フラグで制御します。これはproduction
以外のすべての環境でデフォルトで無効になっています。rakeタスクが実行されると、config.eager_load
はconfig.rake_eager_load
で上書きされ、デフォルトではfalse
になります。つまり、production環境で実行するrakeタスクは、デフォルトではアプリケーションをeager loadingしません。
ファイルがeager loadingされる順序は未定義です。
eager loading中に、RailsはZeitwerk::Loader.eager_load_all
を呼び出します。これはすべてのZeitwerkが管理している依存gemもeager loadされていることを保証します。
単一テーブル継承機能は、lazy loading(遅延読み込み)との相性があまりよくありません。Active Recordが正しく動作するには、STI階層を正しく認識する必要がありますが、正確にはlazy loadingが行われるときにクラスはオンデマンドで読み込まれるのです。
この根本的なミスマッチに対処するためには、STIをプリロードする必要があります。これを実現するオプションはいくつかありますが、それぞれトレードオフがあります。それらを見てみましょう。
STIをプリロードする最も手軽な方法は、設定でeager loadingを有効にすることです。
config.eager_load = true
上の設定は、config/environments/development.rb
とconfig/environments/test.rb
で行います。
この方法はシンプルですが、起動時や再読み込みのたびにアプリケーション全体をeager loadingするので、コストがかかる可能性があります。しかし小規模なアプリケーションでは、このトレードオフの価値があるかもしれません。
階層を定義するファイルを専用のディレクトリに置くことで、概念的にも意味のあるものになります。このディレクトリは名前空間を表すものではなく、STIをグループ化することだけが目的です。
app/models/shapes/shape.rb app/models/shapes/circle.rb app/models/shapes/square.rb app/models/shapes/triangle.rb
この例では、app/models/shapes/circle.rb
で定義するものをShapes::Circle
ではなく引き続きCircle
にしたいとします。理由付けとしては、話を簡単にしたいという個人的な好みかもしれませんし、既存のコードベースのリファクタリングを避けたいからかもしれません。
これは、Zeitwerkのcollapsing(折り畳み)機能を使えば可能です。
# config/initializers/preload_stis.rb shapes = "#{Rails.root}/app/models/shapes" Rails.autoloaders.main.collapse(shapes) # 名前空間ではない unless Rails.application.config.eager_load Rails.application.config.to_prepare do Rails.autoloaders.main.eager_load_dir(shapes) end end
このオプションでは、たとえSTIが利用されていなくても、起動時にこれら少数のファイルをeager loadingおよび再読み込みします。ただし、アプリケーションによほど多くのSTIがない限り、測定可能なインパクトを与えることはありません。
このZeitwerk::Loader#eager_load_dir
メソッドはZeitwerk 2.6.2で追加されました。それより前のバージョンでも、app/models/shapes
ディレクトリをリストアップして、その内容に対してrequire_dependency
を呼び出すことは可能です。
このSTIにモデルが追加・修正・削除された場合、再読み込みは期待通りに動作します。ただし、アプリケーションに別のSTI階層が新たに追加された場合は、イニシャライザを編集してサーバーを再起動する必要があります。
これはオプション2と似ていますが、ディレクトリが名前空間を表します。つまり、app/models/shapes/circle.rb
はShapes::Circle
を定義することが期待されます。
オプション3のイニシャライザは、折り畳み(collapsing)の設定がない点を除けば同じです。
# config/initializers/preload_stis.rb unless Rails.application.config.eager_load Rails.application.config.to_prepare do Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/shapes") end end
トレードオフもオプション2と同じです。
このオプションではファイルの配置を変更する必要はまったくありません。代わりにデータベースを使います。
# config/initializers/preload_stis.rb unless Rails.application.config.eager_load Rails.application.config.to_prepare do types = Shape.unscoped.select(:type).distinct.pluck(:type) types.compact.each(&:constantize) end end
このSTIは、テーブルに存在しない型があっても正しく動作しますが、subclasses
やdescendants
などのメソッドは存在しない型を返すことはありません。
このSTIにモデルが追加・修正・削除された場合、再読み込みは期待通りに動作します。ただし、アプリケーションに別のSTI階層が新たに追加された場合は、イニシャライザを編集してサーバーを再起動する必要があります。
デフォルトのRailsは、指定のファイル名やディレクトリ名がどんな定数を定義すべきかを知るのにString#camelize
を利用します。たとえば、posts_controller.rb
というファイル名の場合はPostsController
が定義されていると認識しますが、これは"posts_controller".camelize
がPostsController
を返すからです。
場合によっては、特定のファイル名やディレクトリ名が期待どおりに活用されないことがあります。たとえばhtml_parser.rb
はデフォルトではHtmlParser
を定義すると予測できます。しかしクラス名をHTMLParser
にしたい場合はどうすればよいでしょうか。この場合のカスタマイズ方法はいくつか考えられます。
最も手軽な方法は、以下のように略語を定義することです。
ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "HTML" inflect.acronym "SSL" end
これによって、Active Supportによる活用方法がグローバルに反映されます。 これで問題のないアプリケーションもありますが、以下のようにデフォルトのインフレクタに上書き用のコレクションを渡して、Active Supportの個別の基本語形をキャメルケース化する方法をカスタマイズすることも可能です。
Rails.autoloaders.each do |autoloader| autoloader.inflector.inflect( "html_parser" => "HTMLParser", "ssl_error" => "SSLError" ) end
しかしデフォルトのインフレクタはString#camelize
をフォールバック先として使っているので、この手法は依然としてString#camelize
に依存しています。Active Supportの活用形機能に一切依存せずに活用形を絶対的に制御したい場合は、以下のようにZeitwerk::Inflector
のインスタンスをインフレクタとして設定します。
Rails.autoloaders.each do |autoloader| autoloader.inflector = Zeitwerk::Inflector.new autoloader.inflector.inflect( "html_parser" => "HTMLParser", "ssl_error" => "SSLError" ) end
この場合は、インスタンスに影響するようなグローバル設定が存在しないので、活用形は1つに決定されます。
カスタムインフレクタを定義して柔軟性を高めることも可能です。詳しくはZeitwerkドキュメントを参照してください。
アプリケーションがonce
オートローダを使わない場合、上記のスニペットはconfig/initializers
に保存できます。たとえば、Active Supportを使う場合はconfig/initializers/inflections.rb
に書き、それ以外の場合はconfig/initializers/zeitwerk.rb
に書くとよいでしょう。
アプリケーションがonce
オートローダを使う場合は、この設定を別の場所に移動するか、config/application.rb
のアプリケーションクラスの本体から読み込む必要があります。once
オートローダーはブートプロセスの早い段階でインフレクタを利用するからです。
これまで見てきたように、自動読み込みパスはトップレベルの名前空間であるObject
を表します。
app/services
を例に考えてみましょう。このディレクトリはデフォルトでは生成されませんが、存在すればRailsは自動的に自動読み込みパスに追加します。
app/services/users/signup.rb
は、デフォルトではUsers::Signup
を定義することになっています。しかしサブツリー全体をServices
名前空間の下に置きたい場合はどうでしょうか。デフォルトの設定では、サブディレクトリapp/services/services
を作成することで実現できます。
しかし、好みにもよりますが、app/services/services
という配置に違和感をぬぐえない人もいるでしょう。app/services/users/signup.rb
というファイルがあれば、シンプルにServices::Users::Signup
が定義されるようにしたいかもしれません。
Zeitwerkは、このユースケースに対応するためにカスタムroot名前空間をサポートしており、main
オートローダーを以下のようにカスタマイズすることで実現できます。
# config/initializers/autoloading.rb # この名前空間は実際に存在する必要がある。 # # この例では、モジュールをその場で定義している。 # 他の場所でモジュールを作成し、その定義を通常のrequireで読み込むことも可能。 # どの場合についても、`push_dir`はクラスまたはモジュールオブジェクトを期待している。 module Services; end Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", namespace: Services)
Rails 7.1より前のバージョンではこの機能をサポートしていませんでしたが、同じファイル内に以下のコードを追加して動かすことは可能です。
# Rails 7.1より前のアプリケーション用追加コード app_services_dir = "#{Rails.root}/app/services" # 必ず文字列にすること ActiveSupport::Dependencies.autoload_paths.delete(app_services_dir) Rails.application.config.watchable_dirs[app_services_dir] = [:rb]
カスタム名前空間はonce
オートローダーにも対応しています。しかし、これはブートプロセスの初期段階で設定されるため、アプリケーションのイニシャライザでは設定できません。代わりに、たとえばconfig/application.rb
の中に入れてください。
Railsエンジンは、親アプリケーションのコンテキストで動作し、エンジンのコードの自動読み込み、再読み込み、eager loadingは親アプリケーションによって行われます。アプリケーションをzeitwerk
モードで実行する場合は、エンジンのコードもzeitwerk
モードで読み込まれます。アプリケーションをclassic
モードで実行する場合は、エンジンのコードもclassic
モードで読み込まれます。
Railsが起動すると、エンジンのディレクトリが自動読み込みパスに追加されますが、自動読み込みという観点からは何の違いもありません。自動読み込みの主な入力は自動読み込みパスであり、そのパスがアプリケーションのソースツリーであるか、エンジンのソースツリーであるかは無関係です。
たとえば、以下のアプリケーションはDeviseを使っています。
% bin/rails runner 'pp ActiveSupport::Dependencies.autoload_paths'
[".../app/controllers",
".../app/controllers/concerns",
".../app/helpers",
".../app/models",
".../app/models/concerns",
".../gems/devise-4.8.0/app/controllers",
".../gems/devise-4.8.0/app/helpers",
".../gems/devise-4.8.0/app/mailers"]
このエンジンが親アプリケーションの自動読み込みを制御するのであれば、これまでどおりにエンジンを書けます。
しかし、エンジンがRails 6や6.1以降をサポートしており、親アプリケーションの自動読み込みを制御しないのであれば、classic
モードとzeitwerk
モードのどちらの場合でも実行可能になるように書かなければなりません。そのためには、以下を考慮する必要があります。
classic
モードで特定の箇所で何らかの定数を確実に読み込ませるためにrequire_dependency
呼び出しが必要な場合は、require_dependency
呼び出しを書く。これはzeitwerk
モードでは不要ですが、zeitwerk
モードでも問題なく動作します。
classic
モードは定数名をアンダースコア化してファイル名を求め("User" -> "user.rb")、逆にzeitwerk
モードではファイル名をcamelize
して定数名を求める("user.rb" -> "User")。両者はほとんどの場合一致しますが、HTMLParser
のように大文字が連続すると一致しなくなります。互換性を保つ最も手軽な方法は、大文字が連続する名前を避けることです(この場合は"HtmlParser"にします)。
classic
モードでは、app/model/concerns/foo.rb
ファイルにFoo
とConcerns::Foo
を両方定義することを容認するが、zeitwerk
モードではFoo
のみの定義しか許さない。互換性のためにはFoo
を定義してください。
zeitwerk:check
タスクを使うと、プロジェクトツリーが上述の命名規則に沿っているかを手軽に手動チェックできます。たとえば、classic
モードからzeitwerk
モードに移行するときや、何かを修正するときにこのタスクを使うと便利です。
% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!
アプリケーションの設定次第では他にも出力される可能性がありますが、末尾に"All is good!"が表示されるかどうかをチェックすれば十分です。
プロジェクトが正しくeager loadingされているかどうかをテストスイートで検証するのはよい方法です。
Zeitwerkの命名規則やその他に発生しうるエラー条件をカバーできます。詳しくはRailsテスティングガイドのeager loadingをテストするを参照してください。
ローダーの振る舞いを追跡するベストの方法は、ローダーの活動を調べることです。
最も手軽な方法は、フレームワークのデフォルトが読み込まれた後で以下をconfig/application.rb
に設定することです。
Rails.autoloaders.log!
これにより、標準出力にトレースが出力されます。
ログをファイルに出力したい場合は、上の代わりに以下を設定します。
Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")
Railsロガーは、config/application.rb
が実行される時点ではまだ利用できません。Railsロガーを使いたい場合は、イニシャライザの設定を以下のように変更します。
# config/initializers/log_autoloaders.rb Rails.autoloaders.logger = Rails.logger
Rails.autoloaders
アプリを管理するZeitwerkのインスタンスは以下で利用できます。
Rails.autoloaders.main Rails.autoloaders.once
以下の述語メソッドは引き続きRails 7でも利用可能で、true
を返します。
Rails.autoloaders.zeitwerk_enabled?
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。