2005-07-13 優しいRailsの育て方 [長年日記]
● キャッシュ機能(caching)
Railsには Page, Action, Fragment という3段階のキャッシュ機能が備わっている。tDiaryを使っていても思うが、修正よりも参照の方が圧倒的に多いコンテンツで毎回cgiで同じコンテンツを吐くのは無駄だなぁ。でもキャッシュを自分で用意するのは面倒だし、フレームワークかWebサーバがやってくれたらなぁ。Railsはそんなかゆい所にも手が届く、Web開発界の万能戦艦ノーチラス号なんです。
- Page: アクション(CGI)の出力を丸ごとHTMLファイルとして保存し、静的コンテンツとして利用
- Action: Pageと同じく全体を保存するが、コンテンツを表示する前に ActionController で filter 操作可能
- Fragment: アクション全体の出力でなく、テンプレート描画の一部分のみをキャッシュ可能
caching するには、config/environment.rb の最後あたりに以下を記述しておく。
# キャッシュ利用の宣言 (これを忘れるとキャッシュされなくて1時間悩む) ActionController::Base.perform_caching = true # キャッシュファイル保存場所を変更する場合 (デフォルトは RAILS_ROOT+'/public') #ActionController::Base.page_cache_directory = '/var/www/rails_cache'
● Page caching 詳細
アクション(cgi)の出力をHTMLで保存し、次回からはActionPack(cgi)の実行を介さずに Web サーバが直接出力する。完全に静的コンテンツ扱いとなるため、通常のコンテンツ生成より100倍くらい速くなりえるが、ステートレスであり、全てのユーザに同じコンテンツを提供するときにしか使えない。具体的には、blog や wiki などには最適だが、個人別のページなどでは恩恵がない。'caches'系クラスメソッドでキャッシュを設定する。
class BerryzController
caches_page :show
def show
@obj = Berryz::find(@params["id"])
end
end
caches_page :show によって、'show'アクション(cgi)の出力結果のHTMLが":page_cache_directory/:controller/:action/:id.html" に保存され、次回以降の参照には直接そのHTMLファイルが返される。キャッシュを無効にする方法は以下の3通り。
- 1. 直接キャッシュファイルを消す (夜間バッチでDBデータが更新されるタイプによさそう)
- 2. expire_page メソッドを呼ぶ (更新ページをもっているコンテンツの場合などに)
- 3. Sweeper を使う (キャッシュ界のシティーハンター)
# 上記2の例
class BerryzController
def update
Berryz.update(@params["berryz"]["id"], @params["berryz"]) # 通常のデータ更新作業
expire_page :action => "show", :id => @params["berryz"]["id"] # ここでキャッシュ削除
redirect_to :action => "show", :id => @params["berryz"]["id"] # 表示することで新しくキャッシュを作る生活の知恵
end
end
● Action caching 詳細
Pageと同じでキャッシュ対象はアクションの出力全部であるが、Fragment(後述)として保存される点と参照時に ActionPack を経由する点が Page とは異なる。つまり、キャッシュを渡す前に ActionPack へ制御がうつるため、そこで認証や制限を行う事が可能になる。
class BerryzController
before_filter :authenticate, :except => :show # filter は全部にかかるので show は除外する
caches_page :show
caches_action :edit
def authenticate
unless @request.env["REMOTE_HOST"] == "127.0.0.1"
redirect_to "/404.html"
end
end
end
キャッシュの削除には expire_action メソッドを利用する。
● Fragment caching 詳細
テンプレートの一部分をキャッシュする。例えば、会員毎のページで会員名を表示するのでPage,Actionは使えないが、共通の重い処理(メニュー項目の作成)だけをキャッシュするという用途がある。PageはHTMLファイルで保存されるが、ActionとFragmentは格納手段(場所)を選択することができる。現在のところ、以下の4種類がある。デフォルトは MemoryStore(メモリ格納)であり、変更する場合は config/environment.rb の最後あたりで定義しておく。
# [MemoryStore] メモリに格納する
# WEBrickやFCGIに最適。CGIではリクエストの最後に破棄されてしまうので無意味。
ActionController::Base.fragment_cache_store =
ActionController::Caching::Fragments::MemoryStore.new
# [FileStore] cache_path にファイルとして保存される。
# どんな環境でも役立つ。
ActionController::Base.fragment_cache_store =
ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory")
# [DRbStore] 別メモリで実行中の共有DRbに保存される。
# 全プロセスで同じキャッシュを共有しつつも、実行は別DRbプロセスで行いたい場合に役立つ。
ActionController::Base.fragment_cache_store =
ActionController::Caching::Fragments::DRbStore.new("druby://localhost:9192")
# [MemCachedStore] Danga's MemCached
# Danga's MemCached を使って DRbStore と同じ動きをする。
ActionController::Base.fragment_cache_store =
ActionController::Caching::Fragments::FileStore.new("localhost")
# (↑FileStoreになってるけど、そういうもの?> actionpack/lib/action_controller/caching.rb)
Fragmentのキャッシュは view 内では cache メソッドとして利用可能。(CacheHelper#cache)
# berryz_mypage.rhtml <b>ようこそ<%= @session["username"] %>さん</b> <% cache do %> 登録データ一覧: <%= render :partial=>"berryz", collection=>Berryz.find_all %> # (注: ← この render の書式は0.13以降) <% end %> # 'collections' => 'collection' に修正(2005/07/18)
controller 内では以下のメソッドが利用可能。(ActionController::Caching::Fragments module)
- cache_erb_fragment(block, name = {}, options = {})
- expire_fragment(name, options = {})
- fragment_cache_key(name)
- read_fragment(name, options = {})
- write_fragment(name, content, options = {})
● Sweeper (Sweeping module)
実際、キャッシュを削除しようとすると、方法1は動的に更新のあるデータを扱うサイトでは使えず、方法2はモデル(データ)が更新される場所(action)ごとに expire_page なりをそれぞれ呼び出していく必要があり、これは結構な作業の上に見落としが入りやすい。そこで方法3の Sweeper の登場です。(但し、Rails-0.13 以降)。これはモデルの状態を見張り、変更があったら after_save イベントに連動してキャッシュを削除するというObserverとFilterのあいのこのようなキャッシュ掃除人です。
# app/models/berryz_sweeper.rb
class BerryzSweeper < ActionController::Caching::Sweeper
observe Berryz
def after_save(record)
expire_page(:controller => "berryz", :action => %w( show ), :id => record.id)
end
end
Berryzモデルを見張り、DBへの保存後に Page caching で作成された show アクション用のHTMLキャッシュを削除する BerryzSweeper を定義している。これだけで使えそうなものだが、実際に上記の Sweeper を使うコントローラ内で利用宣言をしないといけない。
# app/controllers/berryz_controller.rb class BerryzController < ApplicationController caches_page :show cache_sweeper :berryz_sweeper, :only => [ :destroy ] end
cache_sweeper は引数の Sweeper に対して around_filter を準備してくれる。これで BerryzController の destroy アクションの場合にだけさきほどの after_save が実行されることになる。うーん、このコントローラへの登録は冗長じゃないかなぁ?どのコントローラのどのアクションで実行されたときでも、save の後は常にやってくれる方が安全かつ自然のような気がするけど、速度の問題?ARのcallbackのように定義したら常にやって欲しいな。というか、そもそもARの callback でやってしまったらどうだろう。いや、それはモデルがコントローラを知ることになるからMVCに反するからダメか。ARレベルで完結している連続した更新作業を考えても、HTMLキャッシュと連動されると面倒な気もする。結論、概ねよし。
● 今の悩み
例えば、Page caching で "/public/show/*" が大量にあって、関連するモデルのどんな更新でもキャッシュ全部を Sweeper に消してもらいたいとき、どうやるんだろう?
ActionController::Caching::Pages#expire_page(options)
→ ...
→ File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
なのでまとめて消す方法は準備されてないし。Dir[page_cache_directory + page_cache_path(path)]のファイルを全部調べて1つずつoptions 作って expire_page 呼ぶのもなんか違うし。やっぱり、
ActionController::Caching::Pages#expire_page_dir(options)
とか欲しいな。仕方ないからとりあえずは上の方法で泥臭くやろうと思ったら、RAILS_ROOT が空でパスが取れない。WEBrick でやってるからなのか?


いま気づいたのですが、<br># キャッシュ利用の宣言 (これを忘れるとキャッシュされなくて1時間悩む)<br>ActionController::Base.perform_caching = true<br><br>これ、config/environments/development.rbとconfig/environments/production.rbでそれぞれ定義されており、development.rbではfalseとなっているのでキャッシュされません。なのでdevelopment環境でキャッシュ使いたい場合はemviroments/development.rbを書き換えるのが正しいと思います。