Railsでlib以下を読み込ませる方法とその注意点 〜名前重複の死を避けるために〜

  • 123
    いいね
  • 1
    コメント

Rails で lib 以下にライブラリを配置することは多い1と思いますが、その際、何も考えずに autoload_paths を指定すると、思わぬ落とし穴にハマる可能性があります。

具体的には

「ディレクトリを切って、別のネームスペースを与えているにもかかわらず、重複するクラス名があると死ぬ」

という現象に陥ります。

この現象に遭遇したことがある方は、読んでいただけると解決するかと思います。

単に lib 以下を Rails に追加したいんだよ!という方は、結論まで Goto していただければ大丈夫です。

はじめに

Rails に lib を加えるためのコードについて、ざっくり以下の3パターン2を今まで目にしたことがあります。

config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
config/application.rb
config.autoload_paths += Dir["#{config.root}/lib/**/"]
config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]

うち、この中の2パターンでは、lib 以下の任意のディレクトリにあるファイル、および app/xxx 系のディレクトリ直下にあるファイル、これらのファイル名に重複があると死にます。

死にました。

具体例

1番目以外のパターンで以下のようにファイル名に重複があると死にます。

my_app
$ tree app/models/
app/models/
├── super_tool.rb # SuperTool
└── ultra_tool.rb # UltraTool
my_app
$ tree lib/
lib/
├── assets
├── my_tools
│   ├── super_tool.rb # MyTools::SuperTool
│   └── ultra_tool.rb # MyTools::UltraTool
└── tasks
rails_console
[1] pry(main)> SuperTool
LoadError: Unable to autoload constant SuperTool, expected /my_app/lib/my_tools/super_tool.rb to define it

ちゃんとディレクトリ切って別のネームスペース与えてるのに何故!

解説

具体例で挙げた事象について解説していきます。3

エラーの内容

SuperToollib/my_tools/super_tool.rbに定義されていることを期待していますが、実際にはディレクトリ構造に従いMyTools::SuperToolと定義されているため落ちています。

そもそもSuperToolと叩いた時には、app/models/super_tool.rbを見て欲しいのにそうなっていません。

このことから、

  1. SuperToolを評価しようとする
  2. 未ロードの定数であるため、super_tool.rbにマッチする文字列を Rails は探す
  3. libから優先的に探索される
  4. lib/my_tools/super_tool.rbが見つかる
  5. 何故かmy_toolsディレクトリが無視され Rails がこのファイルにSuperToolが定義されてると判断する
  6. 実際にはMyTools::SuperToolと定義されているため落ちる

という流れになっていることが予想できます。

原因を掴むべく順に検証していきます。

Autoload Paths (優先順位)

まずは探索される順番がどうなっているか確認してみます。

$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
/my_app/lib
/my_app/lib/
/my_app/lib/assets/
/my_app/lib/tasks/
/my_app/lib/my_tools/
/my_app/app/assets
/my_app/app/controllers
/my_app/app/helpers
/my_app/app/mailers
/my_app/app/models
/my_app/app/controllers/concerns
/my_app/app/models/concerns

仮説通りlibの優先順位が高いです。

ココで一旦短絡的に「libの中身が探索されるのが問題だから、lib から外しちゃえ!予め読み込んで置いて良いものだし config/initializer にぶち込みじゃ!」と試してみたところ、なんと動きました。

しかし、全くもってワークアラウンドな解決方法なため却下です。

そもそも問題点は優先的に探索されることではなく、「何故かmy_tools/super_tool.rbmy_toolsが無視される」ということです。

Autoload Paths (ディレクトリ)

先ほどの実行結果をまじまじと眺めて見ます。

する時になる部分が……!

/my_app/app/models
/my_app/app/models/concerns

concerns以下のファイルって、Models::Hogeableって書かない……!!

そして、再び lib の部分を見てみます。

/my_app/lib
/my_app/lib/my_tools/

うおお、なるほど、Concerns の件から考えると、これだとmy_tools以下のファイルもMytools::Hogeと書かなくても良いことになりますね……!

そうすると、my_tools/super_tool.rbSuperToolが定義されていることを Rails が期待しているのにも納得がいきます。

根本原因っぽいものにぶち当たりました!万歳!

おそらく、Autoload Paths に格納されているディレクトリをルート (起点) として、そこから Rails のディレクトリ構造 (およびファイル名) とクラス名に関する規約が適用されるということですね!

な、なるほどー!!

原因

以上のことから、原因はコイツだと推定されます。

config/application.rb
config.autoload_paths += Dir["#{config.root}/lib/**/"]

再帰的に lib 以下を全て Autoload Paths にぶち込んでいるため、先ほどのような問題が発生します。

lib 以下もきちんと Rails のレールに乗るって構成するのであれば、再帰的に全てぶち込む必要はありませんね!

解決

原因を取り除いてみます。

まずは設定を変更してみます。

config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

念のためどうなったか確認。

$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
/my_app/lib
/my_app/app/assets
/my_app/app/controllers
/my_app/app/helpers
/my_app/app/mailers
/my_app/app/models
/my_app/app/controllers/concerns
/my_app/app/models/concerns

良さげですね。他の models とかと同じ状態です。

そして実行。

rails_console
[1] pry(main)> SuperTool
class SuperTool < ActiveRecord::Base {
                 :id => :integer,
        :description => :string,
         :content_id => :integer,
         :created_at => :datetime,
         :updated_at => :datetime,
}

動いた!

結論

「lib を Rails に加えるときにどうしたら良いんだよ!」という問いに対する結論は、

config/application.rb
config.autoload_paths << Rails.root.join("lib")

もしくは、

config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

のどちらかをconfig/application.rbに1行加える。これで OK です。

とても普通です。

こすれば lib 以下もきちんと Rails のディレクトリ構造とクラス名の規約に従ってくれます。

例えば、lib 以下が以下の構造をしていた場合、

my_app
21:29:12 $ tree lib/
lib/
├── assets
├── my_tools
│   ├── super_tool.rb
│   └── ultra_tool.rb
└── tasks

Ruby ファイルのクラス名はそれぞれ、MyTools::SuperToolMyTools::UltraToolになります。

ディレクトリ構造とクラス名の規約を無視したい場合

無視と書きましたが、正確に言うと、先ほどのsuper_tool.rbのクラス名を単にSuperToolと書きたい場合の対応方法です。

なかなか稀有な人だと思いますが、その場合は以下の1行を追記すれば大丈夫です。

config/application.rb
config.autoload_paths += Dir["#{config.root}/lib/**/"]

こうすると先ほどのファイルの名前は、SuperToolUltraToolでも動くようになります。

ファイル名とクラス名の規約は残りますが、ディレクトリ構造を無視することができるようになります。

感想

理解不足から、

config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]

と書いていて数時間を棒に振りました……。

全てを完璧に理解しておくというのは現実的ではないとは思いますが、何事も深く理解した上で進めるというのは、やはり大切だなと思いました。

最後は一般論!以上終了!


  1. ルート直下のlibは「一般に使いまわせるもの」を置くべきディレクトリで、そのアプリに特化したものならapp/lib配下に置くのが自然だと思います。大抵の場合、app/libに置くのが自然なことが多い気がします。参考: Railsアプリのモジュールはどこに置くべきか問題 

  2. += %W(#{config.root}/lib)の代わりに<< Rails.root.join("lib")とも書けますが、設定後の状態が同じであるためパターンから除外しています。 

  3. 3番目の設定を採用したと仮定します。