ADZ 學習筆記

Ruby/Rails, Startup, Life

rails 筆記 - eager_load! 與 concerns namespace 問題

| Comments

今天把專案 deploy 上 stagging 的時候 puma start 發現了一個錯誤:

Circular dependency detected while autoloading constant Machine::Base

之前 sidekiq start 也有發生過一樣的事情,經過幾個小時的研究找到這篇文章: https://github.com/rails/rails/issues/11781

environments/*.rb 裡面有一個 config.eager_load = true 只要是 true 就會觸發下面的 eager_load!,這是把專案的 classes, modules 一次 require 進來,通常 production 或 stagging 都是這樣設定的。

# in rails core

def eager_load!
  config.eager_load_paths.each do |load_path|
    matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/
    Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
       require_dependency file.sub(matcher, '\1')
    end
  end
end

其中的 Dir.glob("#{load_path}/**/*.rb").sort 會把專案的 .rb 檔案列出,按照字母排序,依序 require,

問題來了

本人看了 國外的 blog 照著實作把 concerns 的 namespace 以 model 命名,以下是偽扣:

# models/machine.rb

class Machine < ActiveRecord::Base
  include Base
end

# models/concerns/machine.rb

module Machine::Base
  extend ActiveSupport::Concern
  
  included do
    # ... (略)

  end
end

上面的 Machine::BaseMachine 指的是 class Machine < ActiveRecord::Base,目的是 mix 出下面這樣的 class。

class Machine < ActiveRecord::Base
  module Base
    extend ActiveSupport::Concern
    included do 
      # ... (略)

    end
  end
  include Base
end

但因為上 production, stagging 後的載入方式改變,導致出現錯誤,經過 Dir.glob("#{load_path}/**/*.rb").sort 排序後,concerns/*.rbmachine.rb 還要快 require 進來,而 require_dependency 導致錯誤。

我們用 irb 做個小小的範例:

# 範例一:

:001 > module A::B; end
:002 > A.class
=> Module
:003 > class A; end
  TypeError: A is not a class
    from (irb):3
# 範例二:

:001 > class A; end
:002 > A.class
=> Class
:003 > module A::B; end
:004 > A.class
=> Class

意思是如果宣告一個不存在的 namespace 會先把那個 namespace 當成 module,但之後再改宣告成 class 就會錯誤,所以就 rails 的 eager_load! method 在載入時會丟出錯誤。

最後把 concerns 的 namespace 換掉就解決了,這次事件也讓我開始思考 class 和 module 如何跟常用的 rails refactor pattern 和平相處。

controller form objects mailer 的 class 命名慣例是都會帶上 suffix 所以應該沒問題,只有 modelservice objects 要比較小心,model 命名通常都是很簡單的一個單字,service objects 的 convention 也不會帶上 _service.rb

結論

  1. 如果有一批 concerns 只給單一 model,另外開一個專門放的 namespace (或尋求其他讓 model 更 lean 的方法)
  2. service objects 可以用 model 名字當 prefix,因為 sort 順序 models/ > service/

Comments

comments powered by Disqus