Rails の自動読み込みと再読み込み

本書ではZeitwerkモードによって行われる自動読み込み(autoloading: オートロード)と、再読み込み(reloading: リロード)の仕組みについて説明します。

このガイドの内容:

  • 自動読み込みモード
  • 関連するRails設定
  • プロジェクトの構造
  • 自動読み込み、再読み込み、eager loading
  • STI(単一テーブル継承)
  • その他

1 はじめに

本ガイドでは、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ディレクトリさえも管理しません。これらのコードは通常どおりに読み込む必要があります。

2 プロジェクトの構造

Railsアプリケーションで使うファイル名は、そこで定義されている定数名と一致しなければなりません。ファイル名はディレクトリ名と合わせて名前空間として振る舞います。

たとえば、app/helpers/users_helper.rbファイルではUsersHelperを定義すべきですし、app/controllers/admin/payments_controller.rbではAdmin::PaymentsControllerを定義すべきです。

デフォルトのRailsは、ファイル名をString#camelizeメソッドで活用するようZeitwerkを設定します。たとえば、"users_controller".camelizeUsersControllerを返すので、app/controllers/users_controller.rbではUsersControllerという定数が定義されることが期待されます。

このような活用形をカスタマイズする方法については、本ガイドの「活用形をカスタマイズする」で後述します。

詳しくはZeitwerkのドキュメントを参照してください。

3 config.autoload_paths

自動読み込みパス(autoload path: オートロードパス)とは、その中身が自動読み込みの対象となるアプリケーションディレクトリ(app/modelsなど)のリストを指します。これらのディレクトリはルート名前空間であるObjectを表します。

Zeitwerkのドキュメントでは自動読み込みのパスをルートディレクトリと呼んでいますが、本ガイドでは「自動読み込みパス」と呼びます。

自動読み込みパスの下にあるファイル名は、Zeitwerkのドキュメントに記載されているとおりに定義された定数と一致しなければなりません。

デフォルトでは、あるアプリケーションの自動読み込みパスは次のもので構成されています。アプリケーションの起動時にappの下にあるすべてのサブディレクトリ(assetsjavascriptviewsは除く)と、アプリケーションが依存する可能性のあるエンジンの自動読み込みパスです。

たとえば、app/helpers/users_helper.rbUsersHelperが実装されていれば、そのモジュールは以下のように自動読み込み可能になります。したがって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オートローダーによって管理されます。

4 config.autoload_lib(ignore:)

デフォルトでは、libディレクトリはアプリケーションやエンジンの自動読み込みパスに含まれません。

設定メソッドconfig.autoload_libは、libディレクトリをconfig.autoload_pathsconfig.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

5 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/serializersconfig.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で管理されます。

6 config.autoload_lib_once(ignore:)

config.autoload_lib_onceメソッドは、config.autoload_libと似ていますが、libconfig.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

7 再読み込み

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定数に保存されているクラスオブジェクトは、再読み込みすると異なるものに変わります。

7.1 古くなったオブジェクトの再読み込み

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のスーパークラスは元の古いクラスのオブジェクトのままです。

結論: 再読み込み可能なクラスやモジュールをキャッシュしてはいけません

8 アプリケーション起動時の自動読み込み

起動中のアプリケーションは、onceオートローダが管理するautoload_once_pathsからの自動読み込みが可能です(詳しくは前述のconfig.autoload_once_pathsを参照)。

ただし、mainオートローダが管理している自動読み込みパスからの自動読み込みはできません。これは、config/initializersにあるコードや、アプリケーションやエンジンのイニシャライズについても同様です。

イニシャライザーは、アプリケーションの起動時に一度だけ実行されます。再読み込み時に再度実行されることはありません。イニシャライザがリロード可能なクラスやモジュールを使っている場合、それらに対する編集はその初期コードに反映されないため、古くなってしまいます。したがって、初期化時にリロード可能な定数を参照することは禁止されています。

その理由は、イニシャライザはアプリケーション起動時に1度しか実行されないためです。再読み込み時に再実行されることはありません。イニシャライザが再読み込み可能なクラスやモジュールを利用している場合、それらを編集しても初期コードに反映されないため、古くなってしまいます。この理由により、初期化時に再読み込み可能な定数を参照することは禁止されています。

では、代わりにどうすればいいのか見てみましょう。

8.1 ユースケース1: 起動中に、再読み込み可能なコードを読み込む

8.1.1 起動時と各再読み込みの両方で実行される自動読み込み

mainオートローダーが管理しているapp/servicesApiGatewayという再読み込み可能なクラスがあるとします。そしてアプリケーション起動時にエンドポイントを設定する必要が生じたとします。以下のように書くとエラーになります。

# 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回実行される可能性があります。ここで実行するコードは冪等でなければなりません。

8.1.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

8.2 ユースケース2: 起動中に、キャッシュされたままのコードを読み込む

設定によっては、何らかのクラスやモジュールのオブジェクトを受け取って、それを再読み込みされない場所に保存するものもあります。こうした設定では、コードの編集の結果がキャッシュ済みの古いオブジェクトに反映されないため、設定に渡すものは再読み込み可能にしないことが重要です。

そうした例の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-railsActiveRecord::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を参照)。

8.3 ユースケース3: エンジンで使うアプリケーションのクラスを設定する

あるエンジンが、ユーザーをモデリングする再読み込み可能なアプリケーションクラスと連携する場合、そのための設定ポイントが以下のようになっていると、エラーになります。

# 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で現在のクラスオブジェクトを得られるようになります。

9 eager loading

一般的にproduction的な環境では、アプリケーションの起動時にアプリケーションコードをすべて読み込んでおく方が望ましいと言えます。eager loading(一括読み込み)はすべてをメモリ上に読み込むことでリクエストに即座に対応できるように備え、CoW(コピーオンライト)との相性にも優れています。

eager loadingはconfig.eager_loadフラグで制御します。これはproduction以外のすべての環境でデフォルトで無効になっています。rakeタスクが実行されると、config.eager_loadconfig.rake_eager_loadで上書きされ、デフォルトではfalseになります。つまり、production環境で実行するrakeタスクは、デフォルトではアプリケーションをeager loadingしません。

ファイルがeager loadingされる順序は未定義です。

eager loading中に、RailsはZeitwerk::Loader.eager_load_allを呼び出します。これはすべてのZeitwerkが管理している依存gemもeager loadされていることを保証します。

10 STI(単一テーブル継承)

単一テーブル継承機能は、lazy loading(遅延読み込み)との相性があまりよくありません。Active Recordが正しく動作するには、STI階層を正しく認識する必要がありますが、正確にはlazy loadingが行われるときにクラスはオンデマンドで読み込まれるのです。

この根本的なミスマッチに対処するためには、STIをプリロードする必要があります。これを実現するオプションはいくつかありますが、それぞれトレードオフがあります。それらを見てみましょう。

10.1 オプション1: eager loadingを有効にする

STIをプリロードする最も手軽な方法は、設定でeager loadingを有効にすることです。

config.eager_load = true

上の設定は、config/environments/development.rbconfig/environments/test.rbで行います。

この方法はシンプルですが、起動時や再読み込みのたびにアプリケーション全体をeager loadingするので、コストがかかる可能性があります。しかし小規模なアプリケーションでは、このトレードオフの価値があるかもしれません。

10.2 オプション2: 折りたたんだディレクトリをプリロードする

階層を定義するファイルを専用のディレクトリに置くことで、概念的にも意味のあるものになります。このディレクトリは名前空間を表すものではなく、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階層が新たに追加された場合は、イニシャライザを編集してサーバーを再起動する必要があります。

10.3 オプション3: 通常のディレクトリをプリロードする

これはオプション2と似ていますが、ディレクトリが名前空間を表します。つまり、app/models/shapes/circle.rbShapes::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と同じです。

10.4 オプション4: データベースから型をプリロードする

このオプションではファイルの配置を変更する必要はまったくありません。代わりにデータベースを使います。

# 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は、テーブルに存在しない型があっても正しく動作しますが、subclassesdescendantsなどのメソッドは存在しない型を返すことはありません。

このSTIにモデルが追加・修正・削除された場合、再読み込みは期待通りに動作します。ただし、アプリケーションに別のSTI階層が新たに追加された場合は、イニシャライザを編集してサーバーを再起動する必要があります。

11 活用形をカスタマイズする

デフォルトのRailsは、指定のファイル名やディレクトリ名がどんな定数を定義すべきかを知るのにString#camelizeを利用します。たとえば、posts_controller.rbというファイル名の場合はPostsControllerが定義されていると認識しますが、これは"posts_controller".camelizePostsControllerを返すからです。

場合によっては、特定のファイル名やディレクトリ名が期待どおりに活用されないことがあります。たとえば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ドキュメントを参照してください。

11.1 活用形のカスタマイズはどこで行うべきか

アプリケーションがonceオートローダを使わない場合、上記のスニペットはconfig/initializersに保存できます。たとえば、Active Supportを使う場合はconfig/initializers/inflections.rbに書き、それ以外の場合はconfig/initializers/zeitwerk.rbに書くとよいでしょう。

アプリケーションがonceオートローダを使う場合は、この設定を別の場所に移動するか、config/application.rbのアプリケーションクラスの本体から読み込む必要があります。onceオートローダーはブートプロセスの早い段階でインフレクタを利用するからです。

12 カスタム名前空間

これまで見てきたように、自動読み込みパスはトップレベルの名前空間である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の中に入れてください。

13 自動読み込みとRailsエンジン

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モードのどちらの場合でも実行可能になるように書かなければなりません。そのためには、以下を考慮する必要があります。

  1. classicモードで特定の箇所で何らかの定数を確実に読み込ませるためにrequire_dependency呼び出しが必要な場合は、require_dependency呼び出しを書く。これはzeitwerkモードでは不要ですが、zeitwerkモードでも問題なく動作します。

  2. classicモードは定数名をアンダースコア化してファイル名を求め("User" -> "user.rb")、逆にzeitwerkモードではファイル名をcamelizeして定数名を求める("user.rb" -> "User")。両者はほとんどの場合一致しますが、HTMLParserのように大文字が連続すると一致しなくなります。互換性を保つ最も手軽な方法は、大文字が連続する名前を避けることです(この場合は"HtmlParser"にします)。

  3. classicモードでは、app/model/concerns/foo.rbファイルにFooConcerns::Fooを両方定義することを容認するが、zeitwerkモードではFooのみの定義しか許さない。互換性のためにはFooを定義してください。

14 テスト

14.1 手動テスト

zeitwerk:checkタスクを使うと、プロジェクトツリーが上述の命名規則に沿っているかを手軽に手動チェックできます。たとえば、classicモードからzeitwerkモードに移行するときや、何かを修正するときにこのタスクを使うと便利です。

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!

アプリケーションの設定次第では他にも出力される可能性がありますが、末尾に"All is good!"が表示されるかどうかをチェックすれば十分です。

14.2 自動テスト

プロジェクトが正しくeager loadingされているかどうかをテストスイートで検証するのはよい方法です。

Zeitwerkの命名規則やその他に発生しうるエラー条件をカバーできます。詳しくはRailsテスティングガイドeager loadingをテストするを参照してください。

15 トラブルシューティング

ローダーの振る舞いを追跡するベストの方法は、ローダーの活動を調べることです。

最も手軽な方法は、フレームワークのデフォルトが読み込まれた後で以下を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

16 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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加