Фрагментарное кэширование в MVC веб-фреймворках

    Наверняка большинство программистов, работающих с современными веб-фрейворками, реализующими схему MVC, сталкивалось с таким небольшим затруднением: кэширование фрагмента View.

    Хорошие фреймворки предлагают инструменты для полного кэширования страниц, фрагментарного, или кэширования экшенов. Недавно я посмотрел 90 выпуск подкаста Railscasts, посвященный именно фрагментарному кэшированию в Ruby on Rails и уважаемый автор решал проблему, как мне показалось, неоптимально.

    Опишу ситуацию.
    Мы в шаблоне страницы и хотим закэшировать ее часть, например, список новых товаров. Пока все хорошо, мы пользуемся встроенными во фреймворк удобными средствами и в две-три строчки окружаем блок — ура, он кэшируется. Но — чу!, контроллер-то об этом ничего не знает и продолжает выполнять свою работу по подготовке данных для View. Естественно, ведь проверка наличия кэша осуществляется уже из шаблона, а контроллер к тому моменту отработал.

    Автор подкаста показывает некрасивое решение — перенос кода для подготовки данных в шаблон и тут же, естественно, отметает его, как «ugly». Что он предлагает — перенести этот код в модель. То есть, в модели товара создается специальный метод, который выбирает новые товары, и этот метод вызывается из шаблона.
    Это лучше, чем первый вариант, но все же недостаточно хорошо, так как в модели приходится реализовывать вещи, которые могут понадобиться в одном только месте, а при смене интерфейса сайта могут оказаться ненужными и скорее всего останутся болтаться в коде просто так.

    Мое решение


    Я работаю со своим фреймворком на PHP, и пример буду писать на PHP, но решение простое и реализуется на любом скриптовом язке.

    view.php:
    <code>
    ...
    <? if !(cacher::start('Cache_Name')) { ?>
    	<ul>
    		<? foreach ($latest as $item) { ?>
    			<li><?=$item->name();?>: <?=$item->price();?></li>
    		<? } ?>
    	</ul>
    <? cacher::end(); } ?>
    ...
    </code>
    


    controller.php:
    <code>
    ...
    $latest = new model_collection('product');
    $latest->load_by( $condition, $order, $limit );
    
    $this->export('latest', $latest);
    ...
    </code>
    


    Метод load_by(...) выполняет один или несколько запросов к базе данных и формирует набор моделей класса Product. То есть, тратятся ресурсы на запрос, да еще и память на экземпляры модели.
    Хорошо бы как-то запомнить, что мы хотим сделать, а делать это только если кэша нет.
    Напишем это.

    utils.php:
    <code>
    ...
    class prepared extends stdClass // крохотный класс для хранения подготовленной операции
    {
    	// не буду усложнять пример геттерами и сеттерами
    	public $obj, $method, $args;
    }
    
    class utils
    {
    ...
    	public static function prepare( $obj, $method, $args = null )
    	{
    		$res = new prepared();
    
    		// метод принимает неограниченное количество параметров
    		$args = func_get_args();
    		$res->obj = array_shift($args);
    		$res->method = array_shift($args);
    		
    		// запоминаем все остальные параметры
    		$res->args = $args;
    
    		return $res;
    	}
    
    	public static function run( $prepared )
    	{
    		// страховка: шаблон не должен думать, пришли ли реальные данные, или заготовка
    		if (!($prepared instance_of prepared)) return $prepared;
    		
    		return call_user_func_array( array($prepared->obj, $prepared->method), $prepared->args );
    	}
    
    ...
    }
    ...
    </code>
    

    Метод run() упрощен, по подсказке davojan.


    Использование



    controller.php:
    <code>
    ...
    $latest = new model_collection('product');
    // ничего не грузим сразу
    // $latest->load_by( $condition, $order, $limit );
    // запоминаем, что мы хотим сделать, в самой переменной для шаблона
    $latest = utils::prepare( $latest, 'load_by', $condition, $order, $limit );
    
    $this->export('latest', $latest);
    ...
    </code>
    


    view.php:
    <code>
    ...
    <? if !(cacher::start('Cache_Name')) { ?>
    	<?
    	// только здесь выполняем запланированное, при этом шаблону не нужно знать, что именно делается
    	$latest = utils::run( $latest );
    	?>
    	<ul>
    		<? foreach ($latest as $item) { ?>
    			<li><?=$item->name();?>: <?=$item->price();?></li>
    		<? } ?>
    	</ul>
    
    <? cacher::end(); } ?>
    ...
    </code>
    


    Предположим, в вашем фреймворке товары надо было бы грузить статическим методом. Пожалуйста, можно и так:

    controller.php:
    <code>
    ...
    // ничего не грузим сразу
    // $latest = Product::get_latest(...);
    // запоминаем, что мы хотим сделать, в самой переменной для шаблона
    $latest = utils::prepare( 'Product', 'get_latest', ... );
    
    $this->export('latest', $latest);
    ...
    </code>
    


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

    Буду рад комментариям.


    Апдейт


    В комментариях мне указывают на наличие компонентов и возможности кэшировать их целиком. Я вынужден пояснить — заметка не об этом. Приведу другой пример, из реальной жизни.

    Страница со списком новостей, экшен 'index' контроллера 'news'.
    <code>
    ...
    $news = new model_collection('news'); // или как у вас
    $news->load_by( $conditions, $order, $limit );
    
    $this->export('news', $news);
    ...
    </code>
    


    Шаблон со списком новостей вкладывается в лэйаут, в котором присутствует еще куча компонент (новые товары, курсы валют и прочее). Компоненты кэшируются целиком, естественно. Но вот именно «основной» экшен же нам надо выполнить, мы страницу целиком закэшировать чаще всего не можем.
    Тут-то и пригождается описанный подход — данные не доставать сразу, а только приготовиться. Можно, конечно, вынести непосредственно вывод новостей в еще один экшен, но таким путем мы почти удвоим количество экшенов, а это явно неудобно.

    Так должно быть понятней.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 66

      0
      Вообще с точки зрения MVC получается правильно: View посредством run просит отдать ему данные.
      Интересный пример, над которым стоит подумать. Правда субъективно, хочется что-то более элегантное.
      Спасибо за решение :)
        0
        Рад, что понравилось. Насчет элегантности - две строчки ведь, куда уж элегантнее?
          0
          Да я давно уже задаюсь вопросом фрагментарного кэширования в MVC. Философский камень ищу, блин :)
            +1
            настоящая элегантность - это кеширование должно быть вообще прозрачным и для view и для контроллера, а настраиваться где-нибудь отдельно
              0
              Это, между прочим, вопрос - что лучше? Настраивать отдельно (и помнить об этой новой связи) или выполнять действия именно там, где они нужны.
              Потом, во view придется все-таки указать сам блок для кэширования, это и сделано.
              Заметка все равно не об этом, а только о маленьком нюансе.
          0
          Спасибо, вроде все чисто. Надо бы перечитать как-нибудь, а то пока голова не варит :-)
          • НЛО прилетело и опубликовало эту надпись здесь
              –1
              Вот такую бы штуку еще на PHP, и половина сайтов заработала бы быстрее! :) А пока придется кешировать фрагмментарно :(
                +1
                Напишите его на php, велика проблема.
                  –2
                  Ну вообще-то проблема. Решаемая, конечно, но геморная. Ведь надо знать, менялись ли данные в базе. Можно такой мониторинг организовать, но гораздо удобнее, проще пользоваться готовым кешером, который будет работать на уроване СУБД. И новичкам будет проще. И все сайты, в которых нет кеширования, заработают немного быстрее. Мелочь, а приятно :)
                    0
                    >> Решаемая, конечно, но геморная. Ведь надо знать, менялись
                    >> ли данные в базе. Можно такой мониторинг организовать,
                    не могли бы вы порекомендовать пути решения? тоже бьюсь над проблемой "как узнать, expired ли кэш query, не обращаясь к БД" и кроме как "сбрасывать кэш при изменении данных в админке" (вот это действительно ugly :), ничего в голову не приходит
                      +2
                      Варианты есть разные. Вариант 1: поставить триггеры на изменение и добавление данных. При изменении устанавливать флаг в отдельной таблице. Способ наверное самый простой. Проверку флага можно организовать в хранимой процедуре, которая будет возвращать либо кеш, либо выполнит запрос и вернет новые данные, закешировав их. Если кеш не хранится в базе, достаточно просто получать из базы флаг.

                      Второй вариант очевиден. Управлять флагом при внесении изменений в базу из модели данных. Т.е. когда PHP/Python/Ruby/C# или что-то там еще отправило данные в таблицу, выставлять флаг (можно в базе, можно вне базы).

                      А можно и вообще без флагов. При добавлении/изменении данных создавать кеш. Это подойдет, если кеш создается относительно быстро, а добавление/изменение происходит относительно редко.

                      Ну есть вообщем-то и другие варианты, но их я пожалуй опущу...
                0
                Часто запросами дело не ограничивается. Вдобавок, результат запроса-то все же достается из кэша и занимает память просто так, не используясь.
                  0
                  А если данные грепаются не из базы? А, скажем, из сокета?
                  0
                  Меньше-кода, больше-дела, вот это хорошо, спасибо, хорошая статья.
                    0
                    Спасибо за возможность.
                    +1
                    Хотите элегантное решение? Оно есть в symfony. Использование компонентов. Мы просто берем и весь фрагмент указываем как компонент. Компонент это такой мини-контроллер, который сам по себе не запускается, а лишь выдает фрагмент, который встраивается на страницу. Кешировать его можно просто как и обычные фрагменты шаблонов.

                    Короче, нефиг придумывать велосипеды :)
                    + из этитх реализаций мне больше всего нравится реализация через модель. Она самая простая.
                      0
                      Так же сие есть в cakephp.
                        –2
                        В Рельсах от компонентов отказались. Видимо, тому были причины?
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            'news_detail', :collection => @news/News.latest/вставить_нужное %>
                            В рельсах очень обоснованно отказать от компанент потому что они стали попросту не нужны.
                              0

                              <%= render :partial => 'news_detail', :collection => @news/News.latest/вставить_нужное %>
                          +1
                          В моем фреймворке компоненты тоже есть, конечно. И тем не менее, описанная схема полезна. Впрочем, тут не поспоришь - если не возникает необходимости, то пользоваться и не надо. :-)
                            0
                            Написал апдейт, пояснил.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                В django так, кстати, теги устроены.
                                  0
                                  Да, такое у меня есть, именно вызывается отдельный метод какого-то контроллера. Однако на практике так делать удобно не всегда.
                                  0
                                  http://zendframework.com/manual/ru/zend.…

                                  есть такой "фронт энд" Zend_Cache_Frontend_Output
                                  +есть еще и другие "фронт энды"
                                  +в Zend_Cache есть еще понятие бэк эндов, т.е. способов хранения кэша например файл, Memcached, sqlite...
                                  +теги к записям, позволяют гибко управлять очисткой кеша.
                                    0
                                    Я тут писал не про само кэширование, оно-то у меня именно с помощью Zend Cache и сделано. cacher - это мой класс-обертка, для компактности.
                                      0
                                      Да, каюсь, прочитал заголовок только и сразу комментировать... =)
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      На мой взгляд, кэширование SQL запросов как в Рельсе 2, не всегда спасет.

                                      Ведь изменение в БД может производить не только приложение на рельсе, которое управляет и кэшем - к БД может подключаться и стороннее приложение, которое будет вносить изменения в БД, а рельса не будет об этом знать.
                                        0
                                        Вышеописанное касается только кэша, который очищается при изменении данных в БД. Кэш с expires должен при этой схеме работать нормально.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          0
                                          Спасибо.
                                            0
                                            а почему бы не разбить шаблон на блоки, где каждый блок будет иметь контроллер и шаблон? тогда кешировать можно на уровне блоков.
                                              0
                                              Так и делаю, написал выше. Иногда все-таки не все удается разбить, или это породило бы массу дублирования. Пример с последними товарами выбран для простоты. Показательно, что автор подкаста столкнулся именно с проблемой, для которой я предложил решение. То есть это не только мне нужно. ;-)
                                                0
                                                Написал апдейт, пояснил.
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                  0
                                                  Насколько я понял основная фишка здесь паттерн Lazy load (или как-то похоже называется). То есть мы в шаблон передаем не данные, а указываем как их получить. А получаются они только в момент реального использования, то есть при работе с темплейтом.
                                                    0
                                                    Да, похоже.
                                                    –1
                                                    Может я зануда и эстет, НО:
                                                    - вызывать контроллер из представления логически абсурдно
                                                    - пихать в контроллер "модельную" состовляющую - некрасиво

                                                    p.s. брызь читать внимательно: http://ru.wikipedia.org/wiki/Model-view-controller
                                                      0
                                                      И если уж такая пьянка, с вызовом данных из представления, то вместо этого порно - "{block action="Mod_Products.blockNewProducts" limit="10"}", можно было бы сделать так: {model="NewsBlock" view="News"}, т.е. указать какую модель данных выполнить и какое представление наложить на всю эту кашу
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                        0
                                                        Так никто же не вызывает ничего подобного. Код читали?
                                                          0
                                                          Это я коментирующим тута :)
                                                          +1
                                                          Кроме MVC push существует еще MVC pull, который и подразумевает вызов контроллера из вью.
                                                            0
                                                            >> вызывать контроллер из представления логически абсурдно

                                                            почитайте, пожалуйста, Фаулера "Архитектура корпоративных программных приложений", ставшую уже классической.

                                                            Станете еще большим занудой и эстетом, но в более правильном направлении ;)
                                                            0
                                                            Посмотрите пожалуйста http://framework.zend.com/manual/en/zend…

                                                            Вы это хотите реализовать?
                                                              0
                                                              Да, это очень похоже, но не совсем то.
                                                              В документации: $res = $cache->foobar('1', '2');, то есть для выполнения закэшированного нужно все-таки знать, ЧТО должно выполниться.
                                                              У меня же этого знать не нужно. Знаем мы только в контроллере, а в шаблоне всегда одно и то же: $res = utils::run( $res );
                                                              Повторюсь: я и так использую Zend Cache, обернув его своим cacher.
                                                                0
                                                                Правильный выбор товарисч!
                                                              0
                                                              function test()
                                                              {
                                                              if(!$content = $this->CI->cache->load('top_news', 60*60))
                                                              {
                                                              $this->CI->view(shablon) - К примеру

                                                              $this->CI->cache->save($content, 'top_news');
                                                              }
                                                              return $content;
                                                              }

                                                              Получаем нужный результат, все происходит до выполенения View.

                                                              Класс за основу брал отсюда: http://larin.in/archives/11
                                                                0
                                                                Да не то это, это как бы компонент кэшируется целиком.
                                                                У меня про другое совсем, см. в апдейте.
                                                                  0
                                                                  >>Класс за основу брал отсюда: http://larin.in/archives/1
                                                                  :)) наверное все-таки отсюда: http://larin.in/archives/21
                                                                  0
                                                                  Фундаментальная проблема - это то, что контроллер никак не может добраться до этого блока, чтобы "контролировать" его.
                                                                  Мой концепт (action-controller)

                                                                  ...
                                                                  $this->page()->getLayout()->getLayout()->getBlock('spisokTovarov)->аВотТутМожноВсе();
                                                                  ...
                                                                  или:
                                                                  $this->page()->getLayout(2)->getBlock('spisokTovarov)->аВотТутМожноВсе();
                                                                    0
                                                                    ошибся немного - page - это свойство:
                                                                    $this->page->getLayout(2)->getBlock('spisokTovarov)->аВотТутМожноВсе();
                                                                    (ну да ладно:)
                                                                    +1
                                                                    Очень полезная идея, эдакий отложенный запуск экшна. Правда есть подозрение, что если подумать, то можно реализовать это элегантнее.

                                                                    Из замечаний: со статическими вызовом перемудрили, не нужно никаких двух двоеточий, call_user_func умеет принимать на вход массив, в котором первый элемент строка, а не объект - тогда он делает вызов статической функции.

                                                                    А вообще, спасибо за идею.
                                                                      0
                                                                      Спасибо за подсказку про строку, проверю, не знал.
                                                                        +1
                                                                        Ну, собственно, да - элегантнее можно. По крайней мере в том движке, в котором работаю я, все данные к представлению попадают через специальный объект, который называется "контекст" (в шаблоне он называется $c), так вот:

                                                                        Контроллер (было):

                                                                        $this->c->someVar = $this->calculateSomeVar( $argument );

                                                                        Контроллер (стало):

                                                                        $this->c->jitVar( 'someVar', $this, 'calculateSomeVar', $argument );


                                                                        Представление (не изменяется):

                                                                        <?= $c->someVar ?>


                                                                        Как написать __get() надо объяснять?
                                                                          0
                                                                          Ну что ж, действительно, при таком подходе (похоже на Code Igniter, что ли) так будет совсем красиво. У меня-то переменные в шаблоне уже просто как $product, $category, $books и т.п. появляются, так что __call или __get некуда приделывать.
                                                                          Замечательное развитие мысли, спасибо.
                                                                        0
                                                                        все круто но само по себе кеширование кусков кода или результатов функций это просто

                                                                        сложнее гораздо когда один и тот же кусок данных используется в нескольких местах) в этом случае при его изменении необходимо сбрасывать кеш со всеми местами где он используется) вот в таком случае не просто получается
                                                                          0
                                                                          да и вообще КЭШИРОВАТЬ НУЖНО НЕ КУСКИ VIEW а куски данных которые нужны для вью, которые собираются контроллером, кэширование вью это отдельная ступенька
                                                                            0
                                                                            У меня, например, сделано так: кэшируются выборки из базы. Каждая выборка- отдельный объект, который может быть поименован согласно того что именно выбирается (это делается как правило на уровне модели), т.е. контроллер спрашивает у модели "записи пользователя 1", модель возвращает выборку и именует ее. Именование идет каскадно. Соотв. по имени можно сбросить все связанные кэши.

                                                                            Если очень хочеться закэшировать html, то у меня это реализовано приблизительно следующим образом:
                                                                            1. Выборка передается в шаблон, шаблон пробегается по ней в цикле, при этом непосредственно запрос к БД/кэшу происходит on-demand.
                                                                            2. Если мы кэшируем какой-то участок шаблона- если кэш не найден, запоминаются все имена произведенных выборок, после чего запоминаем в кэше собствено html и связи с именованными выборками.
                                                                            3. При изменении модели она сбрасывает все кэши с некоторым именем, как кэши выборок, так и непосредсвенно html.

                                                                            Сам вызов кэша в шаблоне реализован как активный тег поэтому выглядит все как-то так:
                                                                            контроллер

                                                                            $sel= new items_select();
                                                                            $sel->Where('UserID',eq,$UserID);
                                                                            //Имя кэша:
                                                                            $sel->setCacheName('user_items_'.$UserID);
                                                                            //Постраничный вывод:
                                                                            $sel->Limit((int)$_REQUEST['page']*10,10);

                                                                            Шаблон:
                                                                            (тупо делаем имя по строке запроса)

                                                                            <!-- op:cache="user_items_{$_SERVER.REQUEST_URI}" lifetime="1000"-->
                                                                            <!--op:each="$sel"-->
                                                                            <a href="{$Link}">{$Title}</a>
                                                                            <!--/op:each-->
                                                                            <!--/op:cache-->
                                                                              0
                                                                              Написал подробную статью о том как устроены шаблоны Django и как в Django решается сабж.
                                                                                0
                                                                                Обязательно почитаю.

                                                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                              Самое читаемое