概要
Rails 6でER図を出力したかったのですが、Rails 6ではリリースノートにありますようにオートローダーはデフォルトでZeitwerkを用いています。それゆえこれまでと同様にrails-erdのREADMEに従って設定するだけではうまくいかなかったので、その解決方法の備忘録です。
問題
ER図を作成しようとするとモデルがロードできずエラーになります。
$ bin/rake erd filetype=png
Loading application environment...
Loading code in search of Active Record models...
Generating Entity-Relationship Diagram for 0 models...
rake aborted!
No entities found; create your models first!
原因
原因究明を開始したは良かったのですが、最初はそもそもオートローダーが何をどうやってるのかさっぱりでしたし、なんならあまり意識もしていませんでした(恥ずかしながらこれまで何も困ったことがなかったので)。正直今もあまり分かってはいないのですが、Rails 5と比較してどこに違いがあるのかを探っていきました。
Rails.application.eager_load!
rails-erd
のソースコードを読んでいくとすぐに挙動が違うところを見つけました。それがRails.application.eager_load!
です。このメソッドをコールするとオートローダーの設定に関係なく一括でロードしてくれるみたいです。またここで言う設定とは config/environments/xxx.rb
のconfig.eager_load
のことで、こちらに関してはここの説明がとても分かりやすいです。
task :load_models do
say "Loading application environment..."
Rake::Task[:environment].invoke
say "Loading code in search of Active Record models..."
begin
Rails.application.eager_load!
...
end
ではこの Rails.application.eager_load!
にて何が違うのかと言いますと、Rails 5ではロードするパスの配列を返していたのですが、Rails 6では返り値がnil
になっていました。このメソッドの返り値を使っているようなコードは見当たらなかったので、返り値自体が何かに使われるわけではなさそうですが、何かおかしそうだと思って調べてみました。
def eager_load!
# Already done by Zeitwerk::Loader.eager_load_all in the finisher.
return if Rails.autoloaders.zeitwerk_enabled?
config.eager_load_paths.each do |load_path|
# Starts after load_path plus a slash, ends before ".rb".
relname_range = (load_path.to_s.length + 1)...-3
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file[relname_range]
end
end
end
(使いこなせていない)pryでeager_load!
のソースコードを読むとZeitwerk
を使っているとこのRails.application.eager_load!
は何もせず終わってしまうことが分かりました。Rails 6としてはこの挙動は正しいのかもしれませんが、このままではRails.application.eager_load!
を使って読み込むことを前提としたrails-erd
としては困りそうです。
解決策その1 従来のオートローダーを使う
最も簡単な方法は従来のオートローダーを使うことだと思います。従来のオートローダーを使うこと自体はとても簡単で、以下のように config/application.rb
で config.autoloader = :classic
を設定するだけでいけます。
module RailsTest
class Application < Rails::Application
config.load_defaults 6.0
config.autoloader = :classic
end
end
しかし、このままではせっかく今回導入されたZeitwerk
を使えないので少しもったいない感じがします。
解決策その2 なんとかしてZeitwerk::Loader.eager_load_all
を使う
さきほどの lib/rails/engine.rb
にて「Already done by Zeitwerk::Loader.eager_load_all」と言ってるのですからこいつをコールしてあげれば良いのでは?という発想です。以下が代わりに作ったRake Taskです。
namespace :test do
task erd: :environment do
Zeitwerk::Loader.eager_load_all
Rake::Task['erd'].invoke
end
end
実際に使ってみます。
$ bin/rake test:erd
Loading application environment...
Loading code in search of Active Record models...
Generating Entity-Relationship Diagram for 8 models...
Warning: Ignoring invalid model ActionText::RichText (table action_text_rich_texts does not exist)
Warning: Ignoring invalid model ActionMailbox::InboundEmail (table action_mailbox_inbound_emails does not exist)
Done! Saved diagram to erd.png.
ER図が作成されました。しかし、まだこのままではbin/rails g erd:install
で作成したauto_generate_diagram.rake
がロードするマイグレーションのフックには影響を与えません。なのでrails-erd
がマイグレーション時にコールするメソッドをモンキーパッチで書き換えてあげる必要があります。幸いなことにマイグレーション時にコールされる ERDGraph::Migration.update_model
はerd
タスクをinvoke
するだけの簡単なメソッドなのでこれを変更しました。
if Rails.env.development?
RailsERD.load_tasks
module ERDGraph
class Migration
def self.update_model
Zeitwerk::Loader.eager_load_all
Rake::Task['erd'].invoke
end
end
end
end
$ bin/rails db:migrate
Loading application environment...
Loading code in search of Active Record models...
Generating Entity-Relationship Diagram for 8 models...
Warning: Ignoring invalid model ActionText::RichText (table action_text_rich_texts does not exist)
Warning: Ignoring invalid model ActionMailbox::InboundEmail (table action_mailbox_inbound_emails does not exist)
Done! Saved diagram to erd.png.
これでマイグレーションの度に勝手にER図を更新してくれると思います。仮にrails-erd
がRails 6をサポートした場合においてもモジュール配下を削除するだけで済むのでそんなに悪くない手法だと思います。
考察
従来のオートローダーを使うのはやはりもったいない感じがするので、今回は「なんとかしてZeitwerk::Loader.eager_load_all
を使う」であげたモンキーパッチをあてる手法を採用しました。ライブラリが提供するクラスをモンキーパッチで手を加えるのは可能な限り回避すべきかもしれません。ですが、ERDGraph::Migration
を他で使うことは基本的にないと思ったのと、そもそも影響範囲が比較的狭いと思ったからです(たしかRake Taskの実行時しかロードされなかった気がします)。
感想
Rails.application.eager_load!
はrails-erd
以外にも依存しているライブラリはあると思うのですがどうなんでしょう?今回の調査に間違いがなければRails側かGem側のどちらかがなんとかしないといけないと思います。ただGem側を変更する場合はそれほど難しくはないような気がします。
しかしまぁ、こういうライブラリの実装を読んでいくと、知らない書き方や思いもよらない手法など新しい発見があって面白いですね〜。