Rails で lib 以下にライブラリを配置することは多い1と思いますが、その際、何も考えずに autoload_paths を指定すると、思わぬ落とし穴にハマる可能性があります。
具体的には
「ディレクトリを切って、別のネームスペースを与えているにもかかわらず、重複するクラス名があると死ぬ」
という現象に陥ります。
この現象に遭遇したことがある方は、読んでいただけると解決するかと思います。
単に lib 以下を Rails に追加したいんだよ!という方は、結論まで Goto していただければ大丈夫です。
はじめに
Rails に lib を加えるためのコードについて、ざっくり以下の3パターン2を今まで目にしたことがあります。
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
うち、この中の2パターンでは、lib 以下の任意のディレクトリにあるファイル、および app/xxx 系のディレクトリ直下にあるファイル、これらのファイル名に重複があると死にます。
死にました。
具体例
1番目以外のパターンで以下のようにファイル名に重複があると死にます。
$ tree app/models/
app/models/
├── super_tool.rb # SuperTool
└── ultra_tool.rb # UltraTool
$ tree lib/
lib/
├── assets
├── my_tools
│ ├── super_tool.rb # MyTools::SuperTool
│ └── ultra_tool.rb # MyTools::UltraTool
└── tasks
[1] pry(main)> SuperTool
LoadError: Unable to autoload constant SuperTool, expected /my_app/lib/my_tools/super_tool.rb to define it
ちゃんとディレクトリ切って別のネームスペース与えてるのに何故!
解説
具体例で挙げた事象について解説していきます。3
エラーの内容
SuperTool
がlib/my_tools/super_tool.rb
に定義されていることを期待していますが、実際にはディレクトリ構造に従いMyTools::SuperTool
と定義されているため落ちています。
そもそもSuperTool
と叩いた時には、app/models/super_tool.rb
を見て欲しいのにそうなっていません。
このことから、
-
SuperTool
を評価しようとする - 未ロードの定数であるため、
super_tool.rb
にマッチする文字列を Rails は探す -
lib
から優先的に探索される -
lib/my_tools/super_tool.rb
が見つかる - 何故か
my_tools
ディレクトリが無視され Rails がこのファイルにSuperTool
が定義されてると判断する - 実際には
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.rb
のmy_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.rb
にSuperTool
が定義されていることを Rails が期待しているのにも納得がいきます。
根本原因っぽいものにぶち当たりました!万歳!
おそらく、Autoload Paths に格納されているディレクトリをルート (起点) として、そこから Rails のディレクトリ構造 (およびファイル名) とクラス名に関する規約が適用されるということですね!
な、なるほどー!!
原因
以上のことから、原因はコイツだと推定されます。
config.autoload_paths += Dir["#{config.root}/lib/**/"]
再帰的に lib 以下を全て Autoload Paths にぶち込んでいるため、先ほどのような問題が発生します。
lib 以下もきちんと Rails のレールに乗るって構成するのであれば、再帰的に全てぶち込む必要はありませんね!
解決
原因を取り除いてみます。
まずは設定を変更してみます。
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 とかと同じ状態です。
そして実行。
[1] pry(main)> SuperTool
class SuperTool < ActiveRecord::Base {
:id => :integer,
:description => :string,
:content_id => :integer,
:created_at => :datetime,
:updated_at => :datetime,
}
動いた!
結論
「lib を Rails に加えるときにどうしたら良いんだよ!」という問いに対する結論は、
config.autoload_paths << Rails.root.join("lib")
もしくは、
config.autoload_paths += %W(#{config.root}/lib)
のどちらかをconfig/application.rb
に1行加える。これで OK です。
とても普通です。
こすれば lib 以下もきちんと Rails のディレクトリ構造とクラス名の規約に従ってくれます。
例えば、lib 以下が以下の構造をしていた場合、
21:29:12 $ tree lib/
lib/
├── assets
├── my_tools
│ ├── super_tool.rb
│ └── ultra_tool.rb
└── tasks
Ruby ファイルのクラス名はそれぞれ、MyTools::SuperTool
とMyTools::UltraTool
になります。
ディレクトリ構造とクラス名の規約を無視したい場合
無視と書きましたが、正確に言うと、先ほどのsuper_tool.rb
のクラス名を単にSuperTool
と書きたい場合の対応方法です。
なかなか稀有な人だと思いますが、その場合は以下の1行を追記すれば大丈夫です。
config.autoload_paths += Dir["#{config.root}/lib/**/"]
こうすると先ほどのファイルの名前は、SuperTool
とUltraTool
でも動くようになります。
ファイル名とクラス名の規約は残りますが、ディレクトリ構造を無視することができるようになります。
感想
理解不足から、
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
と書いていて数時間を棒に振りました……。
全てを完璧に理解しておくというのは現実的ではないとは思いますが、何事も深く理解した上で進めるというのは、やはり大切だなと思いました。
最後は一般論!以上終了!
-
ルート直下の
lib
は「一般に使いまわせるもの」を置くべきディレクトリで、そのアプリに特化したものならapp/lib
配下に置くのが自然だと思います。大抵の場合、app/lib
に置くのが自然なことが多い気がします。参考: Railsアプリのモジュールはどこに置くべきか問題 ↩ -
+= %W(#{config.root}/lib)
の代わりに<< Rails.root.join("lib")
とも書けますが、設定後の状態が同じであるためパターンから除外しています。 ↩ -
3番目の設定を採用したと仮定します。 ↩