本書ではZeitwerk
モードでの自動読み込み(オートロード)および再読み込みの仕組みについて説明します。
このガイドの内容:
本ガイドでは、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アプリケーションでは上のようなことはしません。アプリケーションのクラスやモジュールはどこででも利用できます。
class PostsController < ApplicationController def index @posts = Post.all end end
通常のRailsアプリケーションでrequire
呼び出しを行うのは、lib
ディレクトリにあるものや、Ruby標準ライブラリ、Ruby gemなどを読み込むときだけです。そのため、これらのような自動読み込みパスに属さないものについてはすべて後述します。
Railsではこの機能を提供するため、いくつものZeitwerkローダーを開発者の代わりに管理しています。
Railsアプリケーションで使うファイル名は、そこで定義されている定数名と一致しなければなりません。ファイル名はディレクトリ名と合わせて名前空間として振る舞います。
たとえば、app/helpers/users_helper.rb
ファイルではUsersHelper
を定義すべきですし、app/controllers/admin/payments_controller.rb
ではAdmin::PaymentsController
を定義すべきです。
デフォルトのRailsは、ファイル名をString#camelize
メソッドで活用するようZeitwerkを設定します。たとえば、app/controllers/users_controller.rb
から以下のようにUsersController
という定数を定義します。
"users_controller".camelize # => 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_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
で管理されます。
$LOAD_PATH{#load_path}
自動読み込みパスは、デフォルトで$LOAD_PATH
に追加されます。ただしZeitwerkは内部で絶対ファイル名を用いており、アプリケーション内では自動読み込み可能なファイルをrequire
呼び出しすべきではないため、それらのディレクトリは実際には不要です。以下のフラグを用いることで、$LOAD_PATH
に自動読み込みパスを追加しないようにできます。
config.add_autoload_paths_to_load_path = false
こうすることで探索が削減され、正当なrequire
が少し速くなる可能性もあります。また、アプリケーションでBootsnapを使っている場合は、このライブラリが不要なインデックスを構築しなくても済むため、必要なメモリ使用量を節約できます。
Railsアプリケーションのファイルが変更されると、クラスやモジュールを自動的に再読み込みします。
正確に言うと、Webサーバーが実行中の状態でアプリケーションのファイルが変更されると、Railsは次のリクエストが処理される直前に、main
オートローダが管理しているすべての定数をアンロードします。これによって、アプリケーションでリクエスト継続中に使われるクラスやモジュールが自動読み込みされるようになり、続いてファイルシステム上の現在の実装が反映されます。
再読み込みは有効にも無効にもできます。この振る舞いを制御するのはconfig.cache_classes
設定です。これはdevelopment
モードではデフォルトでfalse
(再読み込みが有効)、production
モードではtrue
(再読み込みが無効)になります。
デフォルトのRailsは、変更されたファイルをイベンテッドファイルモニタで検出しますが、自動読み込みパスを調べてファイル変更を検出することも可能です。これは、config.file_watcher
の設定で制御されます。
Railsコンソールでは、 config.cache_classes
の値にかかわらずファイルウォッチャーは動作しません(通常、コンソールセッションの最中に再読み込みが行われると混乱を招く可能性があるためです)。一般にコンソールセッションは、 個別のリクエストと同様に変化しない、一貫したアプリケーションクラスとモジュールのセットによって提供されることが望まれます。
ただし、コンソールで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度しか実行されないためです。サーバーを再起動すれば、新しいプロセスで再度実行されますが、再読み込みはサーバーを再起動しないので、イニシャライザは実行されません。主な2つのユースケースを見てみましょう。
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
の変更は反映されないので、先祖への継承チェインには元のオブジェクトが引き続き残ります。
すなわち、こうしたクラスやモジュールは再読み込み可能にできません。
そのようなクラスやモジュールを起動時に参照する最も手軽な方法は、自動読み込みパスに属さないディレクトリでそれらを定義することです。たとえばlib/
に置くのが妥当でしょう。lib/
はデフォルトでは自動読み込みパスに属しませんが、$LOAD_PATH
には属しているので、require
するだけで読み込めます。
別の方法は、上述のように、それらを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(遅延読み込み)との相性があまりよくありません。一般に単一テーブル継承のAPIが正しく動作するには、STI階層を正しく列挙できる必要があるためです。lazy loadingでは、クラスが参照されるまでクラス読み込みは遅延されます。まだ参照されていないものは列挙できないのです。
ある意味、アプリケーションは読み込みモードにかかわらずSTI階層をeager loadする必要があります。
もちろん、アプリケーションが起動時にeager loadするのであれば目的は既に達成されます。そうでない場合、実際にはデータベース内の既存の型をインスタンス化すれば十分です。developmentモードやtestモードであれば普通はこれで問題ありません。これを行う方法の1つは、このモジュールをlib
ディレクトリに配置することです。
module StiPreload unless Rails.application.config.eager_load extend ActiveSupport::Concern included do cattr_accessor :preloaded, instance_accessor: false end class_methods do def descendants preload_sti unless preloaded super end # データベース内にあるすべての型を定数化する。 # その分ディスク容量が余分に必要だが、 # STIのAPIに配慮されていれば実際には問題ではない。 # # store_full_sti_classがtrue(デフォルト)であることが前提 def preload_sti types_in_db = \ base_class. unscoped. select(inheritance_column). distinct. pluck(inheritance_column). compact types_in_db.each do |type| logger.debug("Preloading STI type #{type}") type.constantize end self.preloaded = true end end end end
続いて、プロジェクトのSTIルートクラスでinclude
します。
# app/models/shape.rb require "sti_preload" class Shape < ApplicationRecord include StiPreload # rootクラスにのみ存在する end
# app/models/polygon.rb class Polygon < Shape end
# app/models/triangle.rb class Triangle < Polygon end
デフォルトの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
オートローダーはブートプロセスの早い段階でインフレクタを利用するからです。
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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。