概要
MITライセンスに基づいて要訳・公開いたします。
- 英語記事: Zeitwerk integration in Rails 6 (Beta 2) | Riding Rails
- 原文公開日: 2019/02/22
- リポジトリ: fxn/zeitwerk
- 著者: fxn -- Zeitwerkの作者です。
- ライセンス: MIT
原文はかなり走り書きのようなので、リリースまでのつなぎとご理解ください。
Rails 6 Beta2時点のZeitwerk情報(要訳)
間もなくリリースされるRails 6のBeta2でZeitwerkが統合されます(訳注: 既にBeta2がリリースされました)。
Rails 6の最終版にはZeitwerkの正式なドキュメントが付くことになりますが、それまでは本記事が理解に役立つことでしょう。
Zeitwekの目玉機能
- クラス定義やモジュール定義の定数パスを安定して使えるようになります。
# このクラスの本体のオートロードはRubyのセマンティクスと一致するようになる
class Admin::UsersController < ApplicationController
# ...
end
- 既知の
require_dependency
ユースケースはすべて排除されました。 -
実行順序に依存するオートロード定数のエッジケースも解消しました。
-
オートロードは、現在サポートされているような「Webリクエストの明示的なロック」に限らず、一般的にスレッドセーフになります。たとえば、
bin/rails runner
で実行されるマルチスレッドのスクリプトを書けば問題なくオートロードされるようになります。
他にも、追加コストなしで以下のようなパフォーマンス上のメリットをアプリで得られます。
- 定数のオートロードは、ファイルシステムの相対マッチを探索するためのオートロードパス(path)のスキャンと無縁になりました。Zeitwerkはひとつのパスのみで処理を行い、オートロードは常に絶対ファイル名を用います。さらに、そのひとつのパスからサブディレクトリへは、名前空間が使われている場合にのみ遅延アクセスされます(訳注: 原文のpassはpathの誤りと判断しました)。
-
ファイルシステム上のファイル変更によってアプリで再読み込みが行われても、gemとして読み込まれたエンジンのオートロードパス上にあるコードは再読み込みされません。
-
eager loading: アプリの他に、Zeitwerkが管理するすべてのgemのコードについてもeager loadingされます。
オートロードのモード
Rails 6のオートロードには:zeitwerk
と:classic
という2つのモードがあります。これらについては新たに用意されたconfig.autoloader
で設定されます。
CRubyで動くRails 6では、デフォルトで:zeitwerk
モードになります。以下の設定はconfig/application.rb
で自動的に有効になります。
load_defaults "6.0"
アプリを:classic
モードで動かすには、上のデフォルト設定が読み込まれた後の行で以下を記述します。
config.autoloader = :classic
APIの現状
Zeitwekモードの最初のAPIは収束しつつありますが、現時点では検証の余地が少々残されています。6.0.0.beta2以降のRailsをお使いの場合は、最新のドキュメントをご覧ください。
オートロードパス
オートロードパスの設定ポイントとして、引き続きconfig.autoload_paths
が残されます。アプリの初期化中に手動でActiveSupport::Dependencies.autoload_paths
に設定することもできます。
require_dependency
require_dependency
の既知のユースケースはすべて排除されました。原則としてrequire_dependency
呼び出しはコードベースからすべて削除すべきです。STIについては後述します。
STI
Active Recordで正しいSQLを生成するには、STI階層が完全に読み込まれる必要があります。Zeitwekのプリロード機能はこのユースケースに対応すべく設計されました。
# config/initializers/preload_vehicle_sti.rb
autoloader = Rails.autoloaders.main
sti_leaves = %w(car motorbike truck)
sti_leaves.each do |leaf|
autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
end
階層ツリーの葉(leaf)をプリロードすることで、以後のスーパークラスに至るすべての階層のオートロードがカバーされるようになります。
上述のファイルは起動時にも、再読み込みのたびにもプリロードされます。
Rails.autoloaders
ZeitwerkモードではRails.autoloaders
がenumerableになり、main
とonce
という2つのZeitwekインスタンスを含みます。main
はアプリの制御用であり、once
はgemとして読み込まれるエンジンや、config.autoload_once_paths
で読み込まれるあらゆる未知のコード(これについては今後対応をやめるかも?)の制御用です。Railsはmain
を再読み込みしますが、once
はオートロードとeager loading専用につき再読み込みされません。
2つのインスタンスには以下のように個別にアクセスできます。
Rails.autoloaders.main
Rails.autoloaders.once
しかしRails.autoloaders
がenumerableなのですから、このような直接アクセスが必要になるユースケースはそれほどないでしょう。
オートローダーの挙動のinspect
オートローダーが動いていることを確認したい場合は、config/application.rb
でフレームワークのデフォルト設定が読み込まれた後の行に以下を書きます。
Rails.autoloaders.logger = method(:puts)
callableの他に、Rails.autoloaders.logger=
を使って「引数1つ」でデバッグに応答するもの(通常のロガーと同様)を指定することもできます。
Zeitwerkの挙動をメモリ上の全インスタンス(Rails自身や、gemを制御する可能性のあるもの)について確認したい場合は、config/application.rb
でBundle.require
より前の行に以下を設定します。
Zeitwerk::Loader.default_logger = method(:puts)
後方非互換性について
- 標準の
concerns
ディレクトリ(app/models/concerns
など)に置かれたファイルでは、Concerns
を名前空間化できません。つまり、app/models/concerns/geolocatable.rb
で定義されるのはConcerns::Geolocatable
ではなくGeolocatable
であることが期待されます。 -
アプリがいったん起動すると、オートロードパスはフリーズされます。
-
ActiveSupport::Dependencies.autoload_paths
で指定されたディレクトリが起動時に存在しない場合は無視されます。ここで参照されるのはarrayの実際の要素のみであり、それらのサブディレクトリについては参照されません。起動時にオートロードパスに新しいサブディレクトリが存在する場合は通常どおり問題なくチェックされます(今後変更の可能性あり)。 -
名前空間として振る舞うクラスやモジュールを定義しているファイルでは、それらのクラスやモジュールをそれぞれ
class
キーワードやmodule
キーワードで定義する必要があります。たとえば、app/models/hotel.rb
ファイルでHotel
クラスを定義し、app/models/hotel/pricing.rb
でホテルのmixinを定義しているのであれば、Hotel
クラスをclass
キーワードで定義しなければなりません。また、Hotel = Class.new { ... }
やHotel = Struct.new { ... }
といった書き方もできません。なお、名前空間として振る舞わないクラスやモジュールであれば従来のイディオムでも問題ありません。 -
ひとつのファイルでは、その名前空間に定義する定数を1つに限定すべきです(その内側には定数を複数定義できます)。つまり、
app/models/foo.rb
でFoo
の他にBar
も定義されている場合、Bar
は再読み込みされません。ただしFoo
が再読み込みされるとBar
が再度開かれます。いずれにしろこの動作はclassic
モードでは非常に残念なものですが、ファイル名と同じメインの定数を1つ使うというのがコーディング慣習です。その下に定数を複数置くことはできるので、Foo
では補助的な内部クラスFoo::Woo
を定義してもよいのです。