Rails 6.0.0.rc1でER図を出力

# 概要

Rails 6でER図を出力したかったのですが、Rails 6ではリリースノートにありますようにオートローダーはデフォルトで[Zeitwerk](https://github.com/fxn/zeitwerk)を用いています。それゆえこれまでと同様に[rails-erd](https://github.com/voormedia/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`のことで、こちらに関しては[ここ](https://qiita.com/shakemurasan/items/305bd3d78d67b646bc06)の説明がとても分かりやすいです。

```rails-erd-1.6.0/lib/rails_erd/tasks.rake
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`になっていました。このメソッドの返り値を使っているようなコードは見当たらなかったので、返り値自体が何かに使われるわけではなさそうですが、何かおかしそうだと思って調べてみました。

```railties-6.0.0.rc1/lib/rails/engine.rb
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](https://github.com/pry/pry)で`eager_load!`のソースコードを読むと`Zeitwerk`を使っているとこの`Rails.application.eager_load!`は何もせず終わってしまうことが分かりました。Rails 6としてはこの挙動は正しいのかもしれませんが、このままでは`Rails.application.eager_load!`を使って読み込むことを前提とした`rails-erd`としては困りそうです。

# 解決策その1　従来のオートローダーを使う

最も簡単な方法は従来のオートローダーを使うことだと思います。従来のオートローダーを使うこと自体はとても簡単で、以下のように `config/application.rb` で `config.autoloader = :classic` を設定するだけでいけます。

```config/application.rb
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です。

```lib/tasks/test.rake
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`するだけの簡単なメソッドなのでこれを変更しました。

```lib/tasks/auto_generate_diagram.rake
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側を変更する場合はそれほど難しくはないような気がします。

しかしまぁ、こういうライブラリの実装を読んでいくと、知らない書き方や思いもよらない手法など新しい発見があって面白いですね〜。