Edited at

Rails で Class がうまく読み込まれない問題と戦った話

More than 5 years have passed since last update.


この記事は?

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


chart.rb

class Chart

def initialize(arg1, arg2)
@arg1 = arg1
@arg2 = arg2
end
end


chart/average.rb

class Chart

class Average
end
end


chart/average/set.rb

class Chart

# 平均値の集合を表す Class
class Average::Set
end
end


chart/data.rb

class Chart

# Mix-in 用の Module
module Data
end
end


chart/data/hoge.rb

class Chart

class Data::Hoge
include Data
end
end


chart/data/fuga.rb

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::DataChart::Average::Set などは既存の Class (DataSet) の名前と衝突しており、

その結果うまくロードされていないようです。

一方、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 ディレクトリ以下のファイルが読み込まれるようにしてみました。


config/initializers/requirements.rb

# 上位のディレクトリにあるファイルを優先して 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 するようにしてみると、なぜか解決しました。


config/initializers/requirements.rb

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 するようにしました。


config/initializers/requirements.rb

require Rails.root.join("app/models/chart")



app/models/chart.rb

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


参考


おまけ

最近 A Way to Organize POROs in Rails という記事に感化されています。