Help us understand the problem. What is going on with this article?

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 という記事に感化されています。

QUANON
あんた、マジなんだな?
http://quanon.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした