Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails 6 Beta2時点の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になり、mainonceという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.rbBundle.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.rbFooの他にBarも定義されている場合、Barは再読み込みされません。ただしFooが再読み込みされるとBarが再度開かれます。いずれにしろこの動作はclassicモードでは非常に残念なものですが、ファイル名と同じメインの定数を1つ使うというのがコーディング慣習です。その下に定数を複数置くことはできるので、Fooでは補助的な内部クラスFoo::Wooを定義してもよいのです。

関連記事

Rails: STI(Single Table Inheritance)でハマったところ


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。