10 августа 2011 г.

Простое кеширование вызовов Zend_Db_Table

Кеширование вызовов моделей (да и любых других классов) в зенде классически реализуется с помощью Zend_Cache_Frontend_Class.

В оригинале это выглядит примерно так:

Copy Source | Copy HTML
  1. $model = MyModel();
  2. $model_cached = MyModelCached();
  3.  
  4. $values_direct = $model->doStuff();
  5. $values_cached = $model_cached->doStuff();


Этот путь мне не понравился сразу по нескольким причинам. 
Во первых - удваивание количества сущностей. Когда моделей много и Вы используете не по одной модельке на запрос монструозненько получается.
Во вторых - слишком много настроек кеша. Это очень хорошо и к месту, когда логика кеша сложна или нетривиальна. В моём случае весь кеш работает с одними и теми же настройками и совершать лишних телодвижений по его инстанцированию не хотелось бы.


Реализовано кеширование через расширение класса  Zend_Db_Table_Abstract и магический метод __call.


Для начала загоним настройки нашего дефолтного кеша в регистр (я это делаю в бутстрапе)

Copy Source | Copy HTML
  1. $backendOptions = array(
  2.             'servers' => array( array(
  3.                     'host' => '127.0.0.1',
  4.                     'port' => 11211,
  5.                     'persistent' => true,
  6.                     'weight' => 1,
  7.                     'timeout' => 10,
  8.                     'retry_interval' => 15,
  9.                     'status' => true,
  10.                     'failure_callback' => null ) ) );       
  11. Zend_Registry::set('backendCacheOpt', $backendOptions);



Ну и собственно сама прослойка

Copy Source | Copy HTML
  1. /**
     * Кеширующая прослойка для zend_db_table
     * 
     * Zend_Registry::get('lifeTimeUP')
     * Zend_Registry::get('backendCacheOpt')
     *
     *
     * @desc кэш работает через __call метод
     _{method} для получения данных из БД
     */
  2. abstract class MyCachedDbTable extends Zend_Db_Table_Abstract
  3. {
  4.  
  5.     protected $_cached = null;
  6.  
  7.     public final function init()
  8.     {
  9.         parent::init();
  10.         $this->_setCache();
  11.     }
  12.  
  13.  
  14.     protected function _setCache()
  15.     {
  16.         $this->_cached = Zend_Cache::factory(
  17.                 'Core',
  18.                 'Memcached',
  19.                 array(
  20.                     'lifetime' => Zend_Registry::get('lifeTimeUP'),
  21.                     'automatic_serialization' => true,
  22.                     'caching' => (APPLICATION_ENV == 'production') ? true : false,
  23.                     'cache_id_prefix' => 'dseyeUP_',
  24.                     'ignore_user_abort' => true ),
  25.                 Zend_Registry::get('backendCacheOpt'));
  26.     }
  27.  
  28.     /*
         * проверяет, существует ли метод "_{$method}"
         * пробует считать из кеша
         * сохраняет в кеш
         * кидает исключения
         */
  29.     public function __call( $method, $opt)
  30.     {
  31.        $signature = "{$this->_name}_{$method}_" . md5(serialize($opt)); //сигнатура метода по опциям
  32.         $methodDB = "_{$method}"; //имя метода обращения к ДБ
  33.  
  34.         if( !method_exists($this, $methodDB) )
  35.                 throw new Exception("Method {$methodDB} for DB not found! Cache crash =(");
  36.  
  37.         if( !( $data = $this->_cached->load($signature) ) )
  38.         {
  39.             $data = call_user_func_array(array($this, $methodDB), $opt);
  40.             $this->_cached->save($data, $signature);
  41.         }
  42.  
  43.         return $data;
  44.  
  45.     }
  46.  
  47. }
  48.  


Теперь любая отнаследованная от данной прослойки модель может кешировать свои вызовы.


К примеру таблица новостей

Copy Source | Copy HTML
  1. /*
     * модель новостей
     */
  2. class App_Model_DbTable_News extends MyCachedDbTable
  3. {
  4.  
  5.     protected $_name = 'news';
  6.  
  7.     /*
         * получить несколько последних новостей
         */
  8.     public function _lastOf( $count = 3 )
  9.     {
  10.         $select = $this->select();
  11.         $select->from($this, array( 'id', 'cat', 'title', 'desc', 'date' => 'DATE_FORMAT(`date` , \'%H:%i %d.%m.%Y\')' ))
  12.                ->order('news.date DESC')
  13.                ->limit( (int)$count );
  14.         return $this->fetchAll($select);
  15.     }
  16. }


Теперь вызов метода lastOf данной модельки приведёт к поиску по кешу и отдаче закешированного результата в случае успеха. 
Если необходимо получить свежий результат (тобишь точно не из кеша) - обращайтесь напрямую к методу _lastOf.
Если хотите запретить обращение к методу в обход кеша - сделайте метод _lastOf protected.
Неочевидный суффикс кешируемых методов (нижнее подчёркивание) лучше заменить на чтолибо более очевидное, например noCache_lastOf. У себя я оставил нижнюю черту ради краткости и ввиду того что проект пишу вдвоём =)


Ну и собственно минусы моего решения выходят из его плюсов:
1. Неочевидность логики кеширования без просмотра кода прослойки
2. Жёсткое конфигурирование


Поэтому решение стоит применять с осторожностью.


Комментариев нет: