edge
詳細はrubyonrails.orgで: もっとRuby on Rails

ClassicからZeitwerkへの移行のHOWTO

このガイドでは、Railsアプリケーションを「classic」から「zeitwerk」モードに移行する方法について説明します。

このガイドを読み終えると、以下のことがわかります。

1 「classic」と「zeitwerk」モードとは何ですか?

Railsは初めからRails 5まで、Active Supportで実装されたオートローダーを使用していました。このオートローダーは「classic」として知られ、Rails 6.xでも使用できます。ただし、Rails 7にはこのオートローダーは含まれていません。

Rails 6以降、Railsには新しくてより優れた自動ロードの方法が搭載され、Zeitwerkジェムに委譲されます。これが「zeitwerk」モードです。デフォルトでは、6.0および6.1のフレームワークデフォルトをロードするアプリケーションは「zeitwerk」モードで実行され、Rails 7ではこのモードのみが利用可能です。

2 なぜ「classic」から「zeitwerk」に切り替える必要がありますか?

「classic」オートローダーは非常に便利でしたが、時々オートロードが少しトリッキーで混乱する問題がありました。Zeitwerkは、これを解決するために開発されました。その他の動機もあります。

Rails 6.xにアップグレードする際は、zeitwerkモードに切り替えることを強くお勧めします。classicモードは非推奨です。

Rails 7では、移行期間が終了し、「classic」モードは含まれていません。

3 心配しないでください。

心配しないでください :).

Zeitwerkは、できるだけ「classic」オートローダーと互換性があるように設計されています。現在正常にオートロードされている動作中のアプリケーションがある場合、切り替えは簡単になる可能性があります。多くのプロジェクトが、スムーズな切り替えを報告しています。

このガイドは、自信を持ってオートローダーを変更するのに役立ちます。

何らかの理由で解決方法がわからない状況に遭遇した場合は、rails/railsで問題を開くことをためらわず、@fxnをタグ付けしてください。

4 「zeitwerk」モードをアクティベートする方法

4.1 Rails 5.x以前のアプリケーション

Rails 6.0より前のバージョンのアプリケーションでは、「zeitwerk」モードは利用できません。少なくともRails 6.0である必要があります。

4.2 Rails 6.xで実行されているアプリケーション

Rails 6.xで実行されているアプリケーションには2つのシナリオがあります。

アプリケーションがRails 6.0または6.1のフレームワークデフォルトをロードしており、「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

4.3 Rails 7で実行されているアプリケーション

Rails 7では、「zeitwerk」モードのみが利用可能であり、有効にするための特別な設定は必要ありません。

実際、Rails 7では、setter config.autoloader= は存在しません。もし config/application.rb で使用している場合は、その行を削除してください。

5 アプリケーションが「zeitwerk」モードで実行されていることを確認する方法は?

アプリケーションが「zeitwerk」モードで実行されていることを確認するには、次のコマンドを実行します。

bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'

これが true を出力する場合、zeitwerkモードが有効になっています。

6 アプリケーションがZeitwerkの規約に準拠しているかどうかは?

6.1 config.eager_load_paths

準拠テストは、イーガーロードされたファイルのみで実行されます。したがって、Zeitwerkの準拠性を確認するためには、すべてのオートロードパスをイーガーロードパスに含めることをお勧めします。

これはデフォルトで既に行われていますが、プロジェクトにカスタムのオートロードパスが設定されている場合は、次のようになります。

config.autoload_paths << "#{Rails.root}/extras"

これらはイーガーロードされず、検証されません。イーガーロードパスに追加するのは簡単です。

config.autoload_paths << "#{Rails.root}/extras"
config.eager_load_paths << "#{Rails.root}/extras"

6.2 zeitwerk:check

「zeitwerk」モードが有効になり、イーガーロードパスの設定が再確認されたら、次のコマンドを実行してください。

bin/rails zeitwerk:check

成功した場合、次のように表示されます。

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

アプリケーションの設定によっては、追加の出力がある場合がありますが、最後の "All is good!" が表示されれば問題ありません。 前のセクションで説明したダブルチェックが、実際にはイーガーロードパスの外にいくつかのカスタムオートロードパスが必要であることを確認した場合、このタスクはそれらを検出して警告します。ただし、テストスイートがこれらのファイルを正常にロードする場合は問題ありません。

次に、期待される定数を定義していないファイルがある場合、このタスクはそれを通知します。これは1つのファイルずつ行われます。なぜなら、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.rbVATを定義していますが、オートローダーはVatを期待しているため、なぜですか?

6.3 アクロニム

これは最も一般的な不一致の種類であり、アクロニムに関連しています。なぜこのエラーメッセージが表示されるのかを理解しましょう。

古典的なオートローダーは、欠落している定数の名前であるVATを自動ロードできます。VATunderscoreを適用するとvatが得られ、vat.rbという名前のファイルを探します。これは機能します。

新しいオートローダーの入力はファイルシステムです。vat.rbというファイルが与えられた場合、Zeitwerkはvatに対してcamelizeを呼び出し、Vatを得て、そのファイルが定数Vatを定義することを期待します。これがエラーメッセージの内容です。

これを修正するには、インフレクターにこのアクロニムについて教えるだけです:

# 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!

すべてが正常になったら、プロジェクトをテストスイートで検証し続けることをお勧めします。Check Zeitwerk Compliance in the Test Suiteセクションでは、これを行う方法について説明しています。

6.4 コンサーン

concernsのサブディレクトリを使用して、標準の構造からオートロードおよびイーガーロードすることができます。例えば、

app/models
app/models/concerns

デフォルトでは、app/models/concernsはオートロードパスに属しているため、ルートディレクトリと見なされます。したがって、デフォルトでは、app/models/concerns/foo.rbConcerns::FooではなくFooを定義する必要があります。

アプリケーションがConcernsを名前空間として使用する場合、2つのオプションがあります:

  1. それらのクラスとモジュールからConcerns名前空間を削除し、クライアントコードを更新します。
  2. app/models/concernsをオートロードパスから削除して、現状のままにします:
  # config/initializers/zeitwerk.rb
  ActiveSupport::Dependencies.
    autoload_paths.
    delete("#{Rails.root}/app/models/concerns")

6.5 appをオートロードパスに含める

一部のプロジェクトでは、app/api/base.rbのようにAPI::Baseを定義するためにappをオートロードパスに追加したい場合があります。

Railsはappのすべてのサブディレクトリを自動的にオートロードパスに追加します(一部の例外を除く)。そのため、app/models/concernsと同様に、ネストされたルートディレクトリが存在する状況が発生します。このセットアップはそのままでは機能しません。

ただし、その構造を維持することができます。ただし、イニシャライザでapp/apiをオートロードパスから削除するだけです:

# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
  autoload_paths.
  delete("#{Rails.root}/app/api")

オートロード/イーガーロードするファイルがないサブディレクトリに注意してください。例えば、アプリケーションにはActiveAdminのリソースがあるapp/adminがある場合、それらを無視する必要があります。assetsや関連するディレクトリも同様です:

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(
  "app/admin",
  "app/assets",
  "app/javascripts",
  "app/views"
)

この設定がないと、アプリケーションはこれらのツリーをイーガーロードしようとします。ファイルが定数を定義していないためapp/adminでエラーが発生し、副作用としてViewsモジュールを定義します。

appをオートロードパスに含めることは技術的に可能ですが、少しトリッキーです。

6.6 オートロードされた定数と明示的な名前空間

ファイル内で名前空間がHotelとして定義されている場合、以下のようになります: ``` app/models/hotel.rb # ホテルを定義します。 app/models/hotel/pricing.rb # Hotel::Pricingを定義します。


`Hotel`定数は、`class`または`module`キーワードを使用して設定する必要があります。例えば:

```ruby
class Hotel
end

が良いです。

以下のような代替手段は機能しません。

Hotel = Class.new

または

Hotel = Struct.new

この制限は、明示的な名前空間にのみ適用されます。名前空間を定義しないクラスやモジュールは、これらのイディオムを使用して定義することができます。

6.7 1ファイルに1つの定数(同じトップレベル)

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もリロードされます。

6.8 config.autoload_pathsのグロブ

次のようなワイルドカードを使用した設定に注意してください。

config.autoload_paths += Dir["#{config.root}/extras/**/"]

config.autoload_pathsの各要素は、トップレベルの名前空間(Object)を表す必要があります。これは機能しません。

これを修正するには、ワイルドカードを削除するだけです。

config.autoload_paths << "#{config.root}/extras"

6.9 エンジンからのクラスとモジュールのデコレーション

アプリケーションがエンジンからクラスやモジュールをデコレートしている場合、おそらくどこかで次のようなことを行っています。

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

6.10 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
if Rails.application.config.reloading_enabled?
  Rails.autoloaders.main.on_unload("Country") do |klass, _abspath|
    klass.expire_redis_cache
  end
end

6.11 Springとtest環境

Springは、何か変更があるとアプリケーションコードをリロードします。test環境では、それが機能するためにリロードを有効にする必要があります。

# config/environments/test.rb
config.cache_classes = false

または、Rails 7.1以降:

# config/environments/test.rb
config.enable_reloading = true

そうしないと、次のようなエラーが発生します。

reloading is disabled because config.cache_classes is true

または

reloading is disabled because config.enable_reloading is false

これにはパフォーマンスのペナルティはありません。

6.12 Bootsnap

必ず少なくともBootsnap 1.4.4に依存していることを確認してください。

7 テストスイートでZeitwerkの準拠をチェックする

zeitwerk:checkタスクは、マイグレーション中に便利です。プロジェクトが準拠している場合、このチェックを自動化することをお勧めします。そのためには、アプリケーションをイーガーロードするだけで十分です。

7.1 継続的インテグレーション

プロジェクトに継続的インテグレーションがある場合、テストスイートが実行されるときにアプリケーションをイーガーロードすることが良いアイデアです。アプリケーションが何らかの理由でイーガーロードできない場合、本番よりもCIで知ることができます。

CIは通常、テストスイートが実行されていることを示すためにいくつかの環境変数を設定します。例えば、CIという名前の変数になるかもしれません。

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

Rails 7以降、新しく生成されたアプリケーションはデフォルトでこのように設定されています。

7.2 ベアなテストスイート

プロジェクトに継続的インテグレーションがない場合、Rails.application.eager_load!を呼び出すことでテストスイートでイーガーロードすることができます。

7.2.1 Minitest

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

7.2.2 RSpec

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

8 requireの呼び出しを削除してください

私の経験では、プロジェクトでは一般的にこれを行いません。しかし、いくつかのプロジェクトではこれを行っているのを見たことがあり、他のいくつかのプロジェクトでも聞いたことがあります。 Railsアプリケーションでは、libからのコードやgemの依存関係、標準ライブラリなど、サードパーティからのコードを読み込むためにrequireを使用します。決してautoload可能なアプリケーションコードをrequireで読み込まないでくださいclassicこちらで、なぜこれが悪いアイデアであるかをすでに確認してください。

require "nokogiri" # 良い例
require "net/http" # 良い例
require "user"     # 悪い例、削除してください(app/models/user.rbを想定)

このようなrequire呼び出しを削除してください。

9 利用できる新機能

9.1 require_dependency呼び出しの削除

Zeitwerkでは、require_dependencyのすべての使用例が削除されています。プロジェクトをgrepして、これらの呼び出しを削除してください。

アプリケーションがSingle Table Inheritanceを使用している場合は、Autoloading and Reloading Constants(Zeitwerkモード)ガイドのSingle Table Inheritanceセクションを参照してください。

9.2 クラスとモジュール定義での修飾名の使用

クラスとモジュール定義で定数パスを堅牢に使用できるようになりました。

# このクラス本体でのAutoloadingは、Rubyのセマンティクスに合致します。
class Admin::UsersController < ApplicationController
  # ...
end

注意点として、実行順によっては、クラシックなオートローダーは次のような場合にFoo::Wadusをオートロードできることがあります。

class Foo::Bar
  Wadus
end

これはRubyのセマンティクスに合致していないため、zeitwerkモードではまったく機能しません。このような特殊なケースがある場合は、修飾名Foo::Wadusを使用するか、ネストにFooを追加してください。

class Foo::Bar
  Foo::Wadus
end

または、ネストにFooを追加してください。

module Foo
  class Bar
    Wadus
  end
end

9.3 スレッドセーフ

classicモードでは、定数のオートロードはスレッドセーフではありませんが、Railsにはウェブリクエストのスレッドセーフを実現するためのロックがあります。

zeitwerkモードでは、定数のオートロードはスレッドセーフです。たとえば、runnerコマンドで実行されるマルチスレッドのスクリプトでオートロードを行うことができます。

9.4 イーガーローディングとオートローディングの一貫性

classicモードでは、app/models/foo.rbBarを定義している場合、そのファイルをオートロードすることはできませんが、イーガーローディングは再帰的にファイルを読み込むため、動作します。これは、最初にイーガーローディングでテストを行った場合、後でオートローディングで実行が失敗する可能性があるため、エラーの原因になることがあります。

zeitwerkモードでは、両方のローディングモードが一貫しており、同じファイルで失敗し、エラーが発生します。

フィードバック

このガイドの品質向上にご協力ください。

タイポや事実の誤りを見つけた場合は、ぜひ貢献してください。 開始するには、ドキュメントへの貢献セクションを読んでください。

不完全なコンテンツや最新でない情報も見つかるかもしれません。 メインのドキュメントに不足しているドキュメントを追加してください。 修正済みかどうかは、まずEdge Guidesを確認してください。 スタイルと規約については、Ruby on Rails Guides Guidelinesを確認してください。

修正すべき点を見つけたが、自分で修正できない場合は、 問題を報告してください

そして最後に、Ruby on Railsのドキュメントに関するあらゆる議論は、公式のRuby on Railsフォーラムで大歓迎です。