{ 2010.12.18 }

[rails] config.cache_classes = trueでも特定のクラスだけアクセスの度にloadする

    はてなブックマーク - [rails] config.cache_classes = trueでも特定のクラスだけアクセスの度にloadする
    このエントリーをはてなブックマークに追加

    Ruby技術者認定試験に合格してほっとして倒れそうな山本ですこんにちは。

    (Rails2.xの話になります)
    さて、Railsでは起動時に環境の指定ができますが、productionモードの時はある程度のクラスがキャッシュ化されて読み込みが早くなります。
    通常であれば、config/environments/production.rbに以下の記述があると思います。

    config.cache_classes = true

    これにより、クラスは起動時の一回のみの読み込みとなり高速化が図られてます。

    話はちょっと変わりますが、modelクラスでバリデーションを定義するとき、メッセージを個別に指定したい場合は以下のように書くと思います。

    validates_format_of :hoge, :with => /moge/,  :message => "mogeと書いてください。"

    この時、以下のように

    validates_format_of :hoge, :with => /moge/,  :message => "(#{Time.now}に発生)mogeと書いてください。"

    にして、クラスが読まれたときの時間を表示するといった場合、
    developmentモードでは、その時の時間が表示されるのですが、productionモードでは初回の起動時の時間が表示されたまま、変わることはありません。
    これはproductionモードだとmodelクラスが一度しか読まれないからです。

    そもそも、productionモードでは何が起こっているのか

    ひとえにproductionモードと言っても、ウラでは何が起きているのか。
    その情報を見つけることができなかったため、ソースを追ってみました。

    config.cache_classesは、どこで何らかの処理をされているのか。
    config/environments~以下の各クラスで定義されている、
    config.cache_classesは、どこで判定がされ処理されているのでしょうか、ってことで探してみると以下の場所にありました。

    # Rails2.3.5 rails-2.3.5/lib/initializer.rb
    def initialize_dependency_mechanism
      ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load
    end

    ここで、クラスを読み込むために、requireを使うかloadを使うかのか判定を行っています。
    requireは一度しか読み込みませんが、loadは何回でも読みます。

    特定のクラスだけ読み込む術はないものか

    “指定したクラスだけは、load読み込みにする” なんてのができればいいのでしょうが、一括で起動時にrequireしてしまっているので、どうしようもないのかなとか思いつつ、いろいろやってみました。
    ActiveSupportのDependenciesクラスが色々と鍵を握ってるみたいです。

    まず、controllerにbefore_filterを仕込みその中で、以下のように書いてみました。

    ActiveSupport::Dependencies.load_file("Modelクラスへのパス")

    いかにもなメソッドです。loadし直してくれるのではと、やってみましたが表示時間は変わらず結果は×。

    次に、

    ActiveSupport::Dependencies.load_with_new_constant_marking("Modelクラスへのパス")

    これもいかにもな名前です。が、あえなく×。

    で、

    modelクラスに "unloadable" と宣言する

    ついに見つけました! これを書けば起動時にこのクラスが読まれることはない! はず!

    と意気揚々と試しましたが、×。

    こうなったらと、手当たり次第作戦。
    すると、以下のメソッドをbefore_filterに仕込むと、アクセスの度に、表示する時間が変わりました。

    ActiveSupport::Dependencies.remove_unloadable_constants!

    ついにやったと思いつつ、ソースを見てみます。

    def remove_unloadable_constants!
      autoloaded_constants.each { |const| remove_constant const }
      autoloaded_constants.clear
      explicitly_unloadable_constants.each { |const| remove_constant const }
    end

    どうやら、起動時に読み込んだクラスをリムーブしてるみたいですね。
    これ自体を読んだのであれば、全てが消えてしまう感じでこわいので、ここで使われているremove_constantメソッドだけを使って、該当のクラスを読み込み直しました。

    ActiveSupport::Dependencies.remove_constant "Modelのクラス名"
    load "Modelクラスへのパス"

    すると、晴れてアクセスする度に表示する時間が変わりました。
    #:nodoc:となっていますのでこのメソッドに解説がないのですが、このメソッドは以下のとおり。

    def remove_constant(const)
      return false unless qualified_const_defined? const
     
      const = $1 if /\A::(.*)\Z/ =~ const.to_s
      names = const.to_s.split('::')
      if names.size == 1 # It's under Object
        parent = Object
      else
        parent = (names[0..-2] * '::').constantize
      end
     
      log "removing constant #{const}"
      parent.instance_eval { remove_const names.last }
      return true
    end

    引数で渡したクラス名をチェックしたのち、
    remove_constという記述があり、クラスの定数を削除しているようです。

    一応、他のページ表示や処理にも問題ないようです。

    最適解か

    バリデーションのメッセージを動的に変えたい、という動機からここまでの調査に至りましたが、もっと簡単な方法があるのかもしれません。
    そもそも、どうしてもと言うならcontroller側でエラーメッセージを定義してもいいでしょうし。

    いかんともしがたい理由でmodelをアクセスの度にloadしたい方はご参考に~。

    Comments are closed.