この記事は?
Rails 3.2 で Class (Module) の読み込みで問題が発生して困ったので、
その問題を解決するまでの顛末についてまとめた備忘録です。
具体的には
warning: toplevel constant Y referenced by X::Y
というエラーに対処した話です。
※ Rails 4 では検証していません。
発生した問題について
とある Model のデータの平均値を計算して、その結果をクライアンサイドでチャート表示したい。
それを実現するために app/models 配下に、ActiveRecord::Base を 継承しない Chart というオリジナルの Class を設けました。
その他に平均値を計算したり、チャートを描画するための専用フォーマットのデータを作るための Class が欲しかったので、
そのための設計を行いました。
最終的なファイル構成は以下の通りです。
app/
 └ models/
     ├ chart/
     │  ├ average/
     │  │  └ set.rb
     │  │
     │  ├ data/
     │  │   ├ hoge.rb
     │  │   └ fuga.rb
     │  │
     │  ├ average.rb
     │  └ data.rb
     │
     ├ chart.rb
     │
     ├ model_a.rb
     ├ model_b.rb
     〜
     └ model_n.rb
class Chart
  def initialize(arg1, arg2)
    @arg1 = arg1
    @arg2 = arg2
  end
end
class Chart
  class Average
  end
end
class Chart
  # 平均値の集合を表す Class
  class Average::Set
  end
end
class Chart
  # Mix-in 用の Module
  module Data
  end
end
class Chart
  class Data::Hoge
    include Data
  end
end
class Chart
  class Data::Fuga
    include Data
  end
end
この状態で rails console を起動し、各 Class (Module) を確認すると、
以下のようにいくつかの Class (Module) がうまくロードされていないことが分かりました。
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
=> Chart
[2] pry(main)> Chart::Data
(pry):2: warning: toplevel constant Data referenced by Chart::Data
=> Data
[3] pry(main)> Chart::Data::Hoge
(pry):3: warning: toplevel constant Data referenced by Chart::Data
NameError: uninitialized constant Data::Hoge
from (pry):3:in `<main>'
[4] pry(main)> Chart::Data::Fuga
(pry):4: warning: toplevel constant Data referenced by Chart::Data
NameError: uninitialized constant Data::Fuga
from (pry):4:in `<main>'
[5] pry(main)> Chart::Average
=> Chart::Average
[6] pry(main)> Chart::Average::Set
(pry):6: warning: toplevel constant Set referenced by Chart::Average::Set
=> Set
warning: toplevel constant Data referenced by Chart::Data
なだこれは!!! (CV: 宮野真守)
どうやら Chart::Data や Chart::Average::Set などは既存の Class (Data と Set) の名前と衝突しており、
その結果うまくロードされていないようです。
一方、Chart::Average は名前が衝突していないためか、正しく読み込まれています。
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart::Data.equal?(::Data)
(pry):2: warning: toplevel constant Data referenced by Chart::Data
=> true
[2] pry(main)> Chart::Average::Set.equal?(::Set)
(pry):3: warning: toplevel constant Set referenced by Chart::Average::Set
=> true
※ BasicObject#equal? は2つのオブジェクトが同一のものかどうかを判定するためのメソッドです。
だからと言って、衝突を避けるために Data や Set 以外の名前を使ってしまったら負けかなと思ったので、
この問題を解決すべく奮闘しました。
解決方法: アプリケーション初期化時に require する
Preventing “warning: toplevel constant B referenced by A::B” with namespaced classes in Rails
という Stack Overflow の記事を参考に、
config/initializers ディレクトリを利用して、
アプリケーション初期化時に models/chart ディレクトリ以下のファイルが読み込まれるようにしてみました。
# 上位のディレクトリにあるファイルを優先して require する。
require Rails.root.join("app/models/chart/data")
require Rails.root.join("app/models/chart/data/hoge")
require Rails.root.join("app/models/chart/data/fuga")
require Rails.root.join("app/models/chart/average")
require Rails.root.join("app/models/chart/average/set")
# 代わりに以下のコードでもよい。
# Dir[Rails.root.join("app/models/chart/**/*.rb")].sort.each { |f| require f }
このファイルを追加して再度 rails c で require した Class が
正しく読み込まれているか試してみます。
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
=> Chart
[2] pry(main)> Chart::Data
=> Chart::Data
[3] pry(main)> Chart::Data::Hoge
=> Chart::Data::Hoge
[4] pry(main)> Chart::Data::Fuga
=> Chart::Data::Fuga
[5] pry(main)> Chart::Average
=> Chart::Average
[6] pry(main)> Chart::Average::Set
=> Chart::Average::Set
[7] pry(main)> Chart::Data.equal?(::Data)
=> false
[8] pry(main)> Chart::Average::Set.equal?(::Set)
=> false
おお、うまく読み込まれたみたいですね!
✌ ('ω' ✌ )三 ✌ ('ω') ✌ 三( ✌ 'ω') ✌
ただし、development 環境でサーバを起動 (rails s) してチャートを描画している画面にアクセスするとなぜか
ArgumentError: wrong number of arguments (2 for 0) というエラーが。
上に示したコード例の通り Chart のコンストラクタは2個の引数を受け取るように実装しているのだが、なぜか引数の数が0個になってしまっているのです。
Chart クラス自体はちゃんと読み込まれているのに…謎。
もちろん、コードを Chart クラスを追加する前の状態に戻すと Chart クラスは存在していません。
ですので、既存の Class と衝突している可能性はないはずです。
Loading development environment (Rails 3.2.18)
[1] pry(main)> Chart
NameError: uninitialized constant Chart
試しに config/initializers 以下のファイルで app/models/chart.rb も require するようにしてみると、なぜか解決しました。
require Rails.root.join("app/models/chart") # この行を追加した。
require Rails.root.join("app/models/chart/data")
require Rails.root.join("app/models/chart/data/hoge")
require Rails.root.join("app/models/chart/data/fuga")
require Rails.root.join("app/models/chart/average")
require Rails.root.join("app/models/chart/average/set")
最終的には config/initializers 以下では app/models/chart.rb のみを require するようにし、
app/models/chart.rb でサブディレクトリ以下のファイルを require するようにしました。
require Rails.root.join("app/models/chart")
Dir[Rails.root.join("app/models/chart/**/*.rb")].sort.each { |f| require f }
class Chart
  def initialize(arg1, arg2)
    @arg1 = arg1
    @arg2 = arg2
  end
end
以上で今回作成した Class (Module) が全て正しく読み込まれるようになりました。
その他気になった点
問題に遭遇した当初は、
「development 環境で、サーバを起動して初回アクセス時はチャートの描画処理が正しく動作するのに、
2回目以降のアクセス時には
NameError: uninitialized constant Chart::Data が発生してしまう。」
という謎の問題にも頭を悩まされました。
しかし、記事を書くにあたってその問題を再現させようと試しましたが再現できませんでした…。これまた謎。
この問題は Rails.application.config.cache_classes が関係してるんじゃないかと思ってますが
再現しないために検証できなかったです。ごめんなさい…orz
参考
- [[Qiita] クラスロード問題ではまる]([Stack Overflow] http://qiita.com/tanaka51/items/c8873319689217bb81a9)
- [Stack Overflow] Preventing “warning: toplevel constant B referenced by A::B” with namespaced classes in Rails
[[Stack Overflow] Rails: Elegant way to structure models into subfolders without creating submodules]
(http://stackoverflow.com/questions/1445341/rails-elegant-way-to-structure-models-into-subfolders-without-creating-submodul)
おまけ
最近 A Way to Organize POROs in Rails という記事に感化されています。
