add to hatena hatena.comment (3) add to del.icio.us (0) add to livedoor.clip (6) add to Yahoo!Bookmark (0) Total: 9

CakePHP1.2 Behaviorでモデルのメソッドキャッシュを行う

CakePHP1.2から新たにBehavior(振る舞い)クラスが実装され、モデルに共通の処理を定義することが可能になりました。
今回はBehaviorを使ってモデルの各メソッドの結果を自動的にキャッシュするクラス(CacheBehavior)を検討してみました。
CakePHPでは標準でビューキャッシュをサポートしていますが、Cacheクラスを利用することで簡単にキャッシュ機能を組み込むことができます。


ソースコードは少し長いので、一番最後に記述することにします。

CacheBehaviorの目的

CakePHPでの開発では、
・まず動作するアプリケーションを構築する
・パフォーマンス上ボトルネックになっている部分のチューニングを行う
という手法が一般的だと思います。
そこで、CacheBehaviorはチューニング時に実装していくことを想定し、構築時のソースコードになるべく影響を与えないようにキャッシュ機能が実装できることを目指します。

CacheBehaviorの機能

1.モデル内の任意のメソッドの結果をキャッシュする
  (コントローラーなどの呼び出し側を修正することなく実現したい)
2.キャッシュ有効期限を指定しない場合は、変数にキャッシュする(同一のリクエスト内で有効)
3.モデルのデータ内容に変化(追加・更新・削除)があった場合には自動的にキャッシュをクリアする
4.コントローラーからモデルのメソッドを呼び出す際にキャッシュを実装可能にする
  (とはいえ、コントローラーからも制御したい)

実装準備:Behaviorの登録

CacheBehavior(cache.php)を
/app/models/behaviors/cache.php
に配置。

キャッシュ機能を実装したいモデルの宣言部分にCacheBehaviorを追加。

PHP:
  1. <?php
  2. class SampleModel extends Model{
  3.     var $actsAs    = array('Cache');
  4.     :

実装例1:モデル内の任意のメソッドをキャッシュする

仮に以下のようなメソッドがモデルに登録されているとします。

PHP:
  1. // 引数で渡されたカラムでソートを行い全件を返す
  2. function getAll($order_by = 'id') {
  3.     $data = $this->find('all', array('order' => array($this->name . '.' . $order_by => 'asc')));
  4.     return $data;
  5. }

このメソッドを常にキャッシュする場合は、処理の先頭に以下の4行の記述を追加します。

PHP:
  1. // 引数で渡されたカラムでソートを行い全件を返す
  2. function getAll($order_by = 'id') {
  3.     if ($this->Behaviors->attached('Cache')) {
  4.         $args = func_get_args();
  5.         if($this->cacheEnabled()) return $this->cacheMethod('600', __FUNCTION__, $args);
  6.     }
  7.  
  8.     $data = $this->find('all', array('order' => array($this->name . '.' . $order_by => 'asc')));
  9.     return $data;
  10. }

先頭のIF文はこのモデルにCacheBehaviorが登録されているかをチェックしています。
CakePHP1.2のモデルの機能で各Behaviorは、
  $Model->Behaviors->attach('登録したいBehavior');
  $Model->Behaviors->detach('解除したいBehavior');
とすることで、動的に登録/解除が可能になっています。
その為、処理の途中でCacheBehaviorが解除されている可能性も考慮し登録を確認しています。

実際にキャシュ機能を実装している部分は

PHP:
  1. if($this->cacheEnabled()) return $this->cacheMethod('600', __FUNCTION__, $args);

この部分で、まずCacheBehaviorが利用可能かどうかを、$this->cacheEnabled() で確認し可能であれば、
  return $this->cacheMethod('600', __FUNCTION__, $args);
で処理結果を返します。
(キャッシュが無い場合は自身のメソッドを再実行することになりますが、再帰呼び出しをされた場合にこのcacheEnabled()はFALSEを返します)

cacheMethod()の第1引数はキャッシュ時間(上記例では600秒)でCakePHPのCacheクラスのdurationパラメータになります。
この値が空文字か0の場合は変数キャッシュとなり、CakeコアのCacheEngineは使わずにリクエスト内のみで有効な変数にキャッシュします。

第2引数はキャッシュしたいメソッド名、第3引数はメソッドに渡す引数配列となります。
先に追加した4行のキャッシュを行うソースコードにはメソッド名や引数は全てPHPの別関数やエイリアスで置き換えてますので、全く同じものを他のメソッドにコピーすれば、そのメソッドはキャッシュが有効になります。

実装例2:モデルのfindメソッドをキャッシュする

モデルを利用している全てのfindメソッドに対してキャッシュを利用したい場合は以下のようにfindメソッドをオーバーライドする形式で記述します。

PHP:
  1. // findメソッド全てにキャッシュ(600秒)を利用にする
  2. function find() {
  3.     if ($this->Behaviors->attached('Cache')) {
  4.         $args = func_get_args();
  5.         if($this->cacheEnabled()) return $this->cacheMethod('600', __FUNCTION__, $args);
  6.     }
  7.    
  8.     $parent = get_parent_class($this);
  9.     return call_user_func_array(array($parent, __FUNCTION__), $args);
  10. }

実装例3:コントローラーなどから利用する

コントローラーなどで特定の条件の場合のみキャシュを利用したい場合は、仮に元のソースが以下だったとすると、

PHP:
  1. $option = array('order' => array('Sample.id' => 'asc'));
  2. $data = $this->Sample->find('all', $option);

以下のようにfindメソッドをcacheMethod経由で呼び出すように記述します。

PHP:
  1. $option = array('order' => array('Sample.id' => 'asc'));
  2. $data = $this->Sample->cacheMethod('600','find', array('all', $option));

注意事項

・CacheBehaviorのcacheMethodはクラス名+メソッド名+引数のシリアライズ値によってキャッシュのキーを生成しています。よって、引数のパターン分だけキャッシュファイルが生成されます。
・コアクラスのCacheEngineを利用していますので、キャッシュ方法やパスなどのパラメータはconfigの値が適用されます。
・CacheBehaviorがattachされている状態でモデルのsave()・delete()メソッドが実行されるとキャッシュを自動的に削除します。削除する必要が無い場合は事前に上述のdetach()でBehaviorを解除する必要があります。

ソースコード

/app/models/behaviors/cache.php

PHP:
  1. <?php
  2. class CacheBehavior extends ModelBehavior {
  3.     static $cacheData = array();
  4.     var $enabled = true;
  5.    
  6.     function setup(&$model, $config = array()) {}
  7.    
  8.     /**
  9.      * メソッドキャッシュ
  10.      */
  11.     function cacheMethod(&$model, $expire, $method, $args = array()){
  12.         $this->enabled = false;
  13.         // キャッシュキー
  14.         $cachekey = get_class($model) . '_' . $method . '_'  . $expire . '_' . md5(serialize($args));
  15.        
  16.         // 変数キャッシュの場合
  17.         if(!$expire){
  18.             if (isset($this->cacheData[$cachekey])) {
  19.                 $this->enabled = true;
  20.                 return $this->cacheData[$cachekey];
  21.             }
  22.             $ret = call_user_func_array(array($model, $method), $args);
  23.             $this->enabled = true;
  24.             $this->cacheData[$cachekey] = $ret;
  25.             return $ret;
  26.         }
  27.        
  28.         // サーバーキャッシュの場合
  29.         $ret = Cache::read($cachekey);
  30.         if(!empty($ret)){
  31.             $this->enabled = true;
  32.             return $ret;
  33.         }
  34.         $ret = call_user_func_array(array($model, $method), $args);
  35.         $this->enabled = true;
  36.         Cache::write($cachekey, $ret, $expire);
  37.        
  38.         // クリア用にモデル毎のキャッシュキーリストを作成
  39.         $cacheListKey = get_class($model) . '_cacheMethodList';
  40.         $list = Cache::read($cacheListKey);
  41.         $list[$cachekey] = 1;
  42.         Cache::write($cacheListKey, $list);
  43.         return $ret;
  44.     }
  45.    
  46.     /**
  47.      * 再帰防止判定用
  48.      */
  49.     function cacheEnabled(&$model){
  50.         return $this->enabled;
  51.     }
  52.    
  53.     /**
  54.      * キャッシュクリア
  55.      */
  56.     function cacheDelete(&$model){
  57.         $cacheListKey = get_class($model) . '_cacheMethodList';
  58.         $list = Cache::read($cacheListKey);
  59.         if(empty($list)) return;
  60.         foreach($list as $key => $tmp){
  61.             Cache::delete($key);
  62.         }
  63.         Cache::delete($cacheListKey);
  64.     }
  65.    
  66.     /**
  67.      * 追加・変更・削除時にはキャッシュをクリア
  68.      */
  69.     function afterSave(&$model, $created) {
  70.         $this->cacheDelete($model);
  71.     }
  72.     function afterDelete(&$model) {
  73.         $this->cacheDelete($model);
  74.     }
  75.    
  76. }

関連するその他の記事

Comments

Leave a Reply