問題の共有
railsで例えば、twitter,とrubyzip gemを使う場合を考えてみます。その時Gemfileには以下のように書かれるでしょう
gem 'twitter'
gem 'rubyzip'
bundle install後にrails consoleでrequireされているか確認してみましょう。以下の環境で試しましたが同じ状況です。
- rails 6, 2.4.17
- rails 7.0.6, bundler 2.4.16
root@2fc4af94850b:/usr/src/app# bin/rails c
Running via Spring preloader in process 239
Loading development environment (Rails 6.1.7.4)
irb(main):001:0> defined?(Twitter)
=> "constant" # <- 定義済み。こちらは問題ない
irb(main):002:0> defined?(Zip)
=> nil # <- 未定義!!Gemfileの指定は同じなのになぜ????
irb(main):003:0> require 'zip'
=> true
irb(main):004:0> defined?(Zip)
=> "constant" # <- 参照できたのでLOAD_PATHは間違っていない
Twitter
はきちんと定義されていますが、Zip
は定義されていません。。つまり、タイトルの 「Gemfileのgemが自動requireされない」
が確認できました。
ちょっと待って!そもそもzeitwerkって、gemはrequire必須じゃなかったっけ?
はい、正しいです。 Railsガイドでも明確に書かれています。
Railsアプリケーションでは、requireは「libのコード」「gemなどのサードパーティ依存関係」「標準ライブラリ」の読み込みにしか使いません。アプリケーションのオートロード可能なコードは決してrequireしないでください。
ちなみに 「libのコード」について具体的には ActiveSupport::Dependencies.autoload_paths
で参照されるパスしかautoloadされません。以下rails6の例です。
root@2fc4af94850b:/usr/src/app# bin/rails runner 'puts ActiveSupport::Dependencies.autoload_paths.join("\n")'
Running via Spring preloader in process 208
/usr/src/app/app/channels
/usr/src/app/app/controllers
/usr/src/app/app/controllers/concerns
/usr/src/app/app/helpers
/usr/src/app/app/jobs
/usr/src/app/app/mailers
/usr/src/app/app/models
/usr/src/app/app/models/concerns
/usr/local/bundle/gems/actiontext-6.1.7.4/app/helpers
/usr/local/bundle/gems/actiontext-6.1.7.4/app/models
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/controllers
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/jobs
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/models
/usr/local/bundle/gems/activestorage-6.1.7.4/app/controllers
/usr/local/bundle/gems/activestorage-6.1.7.4/app/controllers/concerns
/usr/local/bundle/gems/activestorage-6.1.7.4/app/jobs
/usr/local/bundle/gems/activestorage-6.1.7.4/app/models
/usr/src/app/test/mailers/previews
このようにrailsガイドにgemファイルはrequireしてね、と書いてあるのになぜGemfileに記載のgemが自動でrequireされているかですが、これは bundlerの仕組み(Bundler.require)のおかげ
です。詳細は以下の記事などご参照ください。
じゃあGemfileに書いておけばrequireいらんのじゃ?
はい、ただしうまくrequireされないパターンがあります。これは 実際にrequireしている箇所を見れば明らか です。1
...
begin
# Loop through all the specified autorequires for the
# dependency. If there are none, use the dependency's name
# as the autorequire.
Array(dep.autorequire || dep.name).each do |file|
# Allow `require: true` as an alias for `require: <name>`
file = dep.name if file == true
required_file = file
begin
Kernel.require file
rescue RuntimeError => e
raise e if e.is_a?(LoadError) # we handle this a little later
raise Bundler::GemRequireError.new e,
"There was an error while trying to load the gem '#{file}'."
end
end
rescue LoadError => e
raise if dep.autorequire || e.path != required_file
if dep.autorequire.nil? && dep.name.include?("-")
begin
namespaced_file = dep.name.tr("-", "/")
Kernel.require namespaced_file
rescue LoadError => e
raise if e.path != namespaced_file
end
end
end
...
ここで、実際にrequireしていることを抜粋すると
file = dep.name if file == true
required_file = file
begin
Kernel.require file
...
と、 dep.name
をもとにKernel.requireしていることが確認できます。 dep.name
はデフォルトではGemfileのgem名前なので、
-
twitter
の場合はrequire 'twitter'
なのでrequireされる -
rubyzip
の場合はrequire 'zip'
でrequire されるべきだがrequire 'rubyzip'
となるのでrequire失敗する
のでした。
そこで動作を確認するために gem 'rubyzip', require: true
と明示的にrequireするように指定してrails consoleを起動すると
root@2fc4af94850b:/usr/src/app# bin/rails c
<internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require': cannot load such file -- rubyzip (LoadError)
from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
from /usr/local/bundle/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:17:in `require'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:60:in `block (2 levels) in require'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:55:in `each'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:55:in `block in require'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:44:in `each'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:44:in `require'
from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler.rb:187:in `require'
from /usr/src/app/config/application.rb:7:in `<top (required)>'
from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:92:in `preload'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:166:in `serve'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:148:in `block in run'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:142:in `loop'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:142:in `run'
from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application/boot.rb:19:in `<top (required)>'
from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
from -e:1:in `<main>'
予想通り cannot load such file -- rubyzip (LoadError)
となりました。
これを回避するには ソースコード にもドキュメント にも書いてありますが gem 'rubyzip', require: 'zip'
とrequireで名前を指定してあげればよいです。
gem 'twitter'
gem 'rubyzip', require: 'zip'
原因を紐解いてみるとシンプルですが、使えて当たりまえな便利な機能が想定外の動きをすると、前提知識があるため無駄に調査時間がかかりますね。。。とはいえ、昔を知っている身からはzeitwerkのおかげでだいぶ楽になった感🥰
まとめ
Gemfileに書いたgemが自動でrequireしない場合は、以下の3つを確認すること
-
gem 'twitter', require: false
と、明示的にrequireしないよう宣言していないか? -
gem 'rubyzip'
のようなgemの名前とrequireされるモジュール名が異なるgem
ではないか? - 動作させたいRails環境(test, development...など)と、bundlerのgroupが異なっていないか?2