CakePHPでモデルキャッシュを利用する
2009.04.01 / php
Cakeでキャッシュ周りの調査をしていたら、モデルのメソッドの実行結果をキャッシュさせるbehaviorがあるのを見つけました。
これが相当いい感じなので、その利点や導入方法についてまとめておきたいと思います。
コントローラのスリム化
MVCモデルでキャッシュを利用しようという話になると、大抵Controllerでキャッシュヒットの有無を確認して、ヒットしない場合キャッシュをリセットする、というロジックがまず頭に浮かぶと思います。
if (($posts = Cache::read('posts')) === false) { $posts = $this->Post->find('all'); Cache::write('posts', $posts); }
ただ、コントローラで毎回このようなキャッシュヒットを確認していると同じようなコードがあちこちに散らばることになるので、保守性が悪くなります。なので、こういうキャッシュ周りの処理はできるだけモデルに振ってしまったほうがよいです。また、基本的なフロー以外の余計なロジックを考えなずにくてよいので、可読性も格段に上がると思います。
// キャッシュヒットすればキャッシュから、ヒットしない場合はDBアクセス $posts = $this->Post->find('all');
Paginationもキャッシュできる
CakePHP1.2から導入されたPaginationですが、実態はfind(‘all’)+ページ情報をセットする処理を隠蔽したものとなっています。ポイントは、$controller->paginate()メソッドの中で、これらの処理を全部行いつつも、returnで戻ってくるのはfind(‘all’)の結果なので、paginate()の結果だけキャッシュさせていても、ページ情報がキャッシュされずにView側でエラーになる、ということです。(このあたりのページ処理についてはcake/libs /controller/controller.phpの1056行目あたりにあります) この理由から、controller側でpaginateの結果をキャッシュさせる作戦はうまくいかないので、Paginationは毎回遅くなります。
そこで、controller側ではなく、Model側でキャッシュを行わせます。paginate()内で行われるfind以外の処理、つまりページ情報の処理についてはその処理時間は高々知れているので、結局findの処理をキャッシュさせておくことでpaginate()の高速化が期待できます。
導入方法
今回は、モデルキャッシュをmemcachedにキャッシュさせるようにしました。コードは上記リンクから入手できるので、app/models/behaviorにcache.phpで保存しておきます。
まず、app/config/core.phpのCache::configを次のようにFileエンジンの設定をコメントアウトし、Memcacheエンジンを利用します。
Cache::config('default', array( 'engine' => 'Memcache', //[required] 'duration'=> 3600, //[optional] 'probability'=> 100, //[optional] 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string 'servers' => array( '127.0.0.1:11211' // localhost, default port 11211 ), //[optional] 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) )); //Cache::config('default', array('engine' => 'File'));
そして、キャッシュを利用したモデルにおいてactsAsで設定します。
var $actsAs = array('Cache');
findをオーバーライドします。僕は、findの第一引数にいろんなバリエーションを持たせることをよくやるので、
function find($type, $queryData = array()){ switch ($type) { case 'popular' : return $this->find('all', Set::merge(array("conditions"=>array("Model.rating"=>5)), $queryData)); case 'latest' : return $this->find('all', Set::merge(array("limit"=>10, "order"=>"Model.created"), $queryData)); default: $args = array($type, $queryData); if ($this->Behaviors->attached('Cache')) { if($this->cacheEnabled()) { return $this->cacheMethod($cache_time, __FUNCTION__, $args); } } $parent = get_parent_class($this); return call_user_func_array(array($parent, __FUNCTION__), $args); } }
こんなかんじで、第一引数のタイプをcase文で振って、return文では素のfind(‘all’)を通らせることで必ずdefaultに処理を落とすようにさせ、そこでキャッシュをかけるようにさせます。これでシンプルでスッキリした構成にできました。
まとめ
1つ思ったのは、このビヘイビアではcacheMethodメソッドにおいて第一引数ではキャッシュ時間を指定させるのですが、この時間は第三引数にしてオプション扱いにしてほしかったかなぁということ。と、いうのは単純な話で、このメソッドにおいて重要度は __FUNCTION__ > $args > $cache_timeであるから。組み込みのエンジンを利用できるのだからcacheのexpireもCache::configのduration値を標準で見てくれてもいいのかな、と思います。
とはいえ、これでコントローラ側では普通にfindを呼び出すだけでキャッシュ付きの処理を行うことができるので、非常に有効なライブラリかと思います。ちなみにキャッシュはModelに対応したテーブルのレコードが変化するたびにクリアされる(=afterSaveをフックしている)ので、頻繁に saveが起こるようなModelでは、あまりキャッシュが有効にならない場合も多いかと思うので、その点はご注意を。