Расширение системных (и не только) таблиц в MODX Revolution

от автора

В настоящий момент занимаюсь переделкой одного новостного портала на MODX Revolution. Так как посещаемость на сайте бывает до 100 000 человек в сутки, вопрос производительности здесь один из самых важных. С учетом того, что на текущий момент в базе более 75 000 статей, при неправильном (и даже при традиционном подходе к разработке на MODX) тормоза сайта практически гарантированы, а если частота посещений превысит время выполнения запроса, то сервер вообще ляжет. Вот часть приемов задействованных здесь для решения этих проблем я и опишу в этой статье.

1. Долгая генерация кеша.

Наверняка многие знают, что при обновлении кеша MODX проходится по всем документам и набивает карту ресурсов в кеш контекста. Если кто не в курсе, подробно я писал про это здесь. И хотя в MODX начиная с версии 2.2.7 (или в районе той) можно в настройках отключать кеширование карты ресурсов (системная настройка cache_alias_map) проблема эта решается только частично — MODX не кеширует УРЛы документов, но структуру с ID-шниками фигачит все равно, перебирая все документы из базы данных. Это приводит к тому, что во-первых, кеш-файл контекста разрастается, а во-вторых, скрипт может просто не выполниться за 30 секунд и кеш-файл побьется, что может вообще привести к фатальным ошибкам и сделать сайт нерабочим.

Но даже если сервер все-таки в состоянии дернуть все документы и набить все в кеш, давайте посмотрим на сравнительные цифры на один запрос при разных настройках. Цифры эти будут весьма относительные ибо многое зависит от настройки сервера и на разных серверах потребление памяти у одного и того же сайта будет разное, но в сравнении эти цифры дадут представление о разнице состояний. Для оценки потребления памяти буду вызывать getdata-процессор на получение 10-ти статей.

Итак, вариант первый: Полное кеширование карты ресурсов включено.

Размер кеш-файла контекста: 5 792 604 байт.
Потребление памяти при запросе: 28,25 Mb
Время: 0,06-0,1 сек.

Вариант второй: Полное кеширование карты ресурсов отключено (системная настройка cache_alias_map == false).

Размер кеш-файла контекста: 1 684 342 байт.
Потребление памяти при запросе: 15,5 Mb
Время: 0,03-0,06 сек.

Вариант третий: Полностью отключено кеширование карты ресурсов патчем cacheOptimizer.

Размер кеш-файла контекста: 54 945 байт.
Потребление памяти при запросе: 4,5 Mb
Время: 0,02-0,03 сек.

И это всего лишь на 75 000 ресурсов. На сотнях тысяч разница будет гораздо ощутимей.

Есть конечно тут и минусы. Например не будет работать Wayfinder, который строит менюшку на основе данных карты алиасов. Здесь придется самому менюшку собирать. Я чаще всего использую menu-процессор, про который писал здесь (см. раздел 2. Замена Wayfinder).

2. Низкая производительность из-за TV-параметров документов.

А вот это основная и наиболее интересная причина написания данного топика. Наверно нет ни одного MODX-разработчика, который бы не использовал телевизоры TV-поля. Они решают сразу две проблемы: 1. добавляют пользовательские поля документам, 2. дают различные интерфейсы для их редактирования в зависимости от типа поля.

Но есть у них и серьезный минус — все они хранятся в одной таблице. Это добавляет сразу несколько проблем:

1. Нельзя управлять уникальностью значений на уровне базы данных.

2. Нельзя использовать различные типы данных для различных TV-полей. Все данные TV-полей содержатся в единой колонке value с типом данных mediumtext. То есть мы и большего объема данные не можем использовать, и числовые значения у нас будут храниться как строчные (что накладывает дополнительные требования к формированию запроса с сортировкой), и сравнение данных из различных колонок у нас не по фэншую, и вторичные ключи не настроить и много-много еще всего неприятного из-за этого.

3. Низкая производительность при выборке из нескольких таблиц. К примеру, у нас для одного документа есть несколько TV-полей, из которых хотя бы 2-3 поля практически всегда заполнены. Хотим мы получить в запросе сразу данные и документов и полей к ним. У нас есть два основных варианта формирования запроса на это:

1. Просто приджоинить таблицу TV-шек.

$q = $modx->newQuery("modResource"); $alias = $q->getAlias(); $q->leftJoin("modTemplateVarResource", "tv", "tv.contentid = {$alias}.id"); $c->select(array(     "tv.*",     "{$alias}.*", )); 

Но здесь есть серьезный минус: в результирующую таблицу мы получим C*TV число записей, где C — кол-во записей в site_content, а TV — количество записей в таблице site_tmplvar_contentvalues для каждого документа в отдельности. То есть, если у нас, к примеру, 100 записей документов и по 3 записи TV на каждый документ (в среднем), то мы получим в итоге 100*3 = 300 записей.

Так как по этой причине в результате на один документ приходилось более одной результирующей записи, то на уровне PHP приходится дополнительно обрабатывать полученные данные чтобы сформировать уникальные данные. Это у нас и в getdata-процессоре выполняется. А это так же увеличивает нагрузку и увеличивает время выполнения.

Вот у меня в этом новостном портале как раз и было в среднем по 3 основных записи на документ. В итоге ~225 000 записей ТВ. Даже с оптимизацией запросов выполнение с условиями занимало 1-4 секунды, что очень долго.

2. Джоинить каждое TV-поле по отдельности.
Примерный запрос:

$q = $modx->newQuery("modResource"); $alias = $q->getAlias(); $q->leftJoin("modTemplateVarResource", "tv1", "tv1.tmplvarid = 1 AND tv1.contentid = {$alias}.id"); $q->leftJoin("modTemplateVarResource", "tv2", "tv2.tmplvarid = 2 AND tv2.contentid = {$alias}.id"); // ......... $c->select(array(     "tv1.value as tv1_value",     "tv2.value as tv2_value",     "{$alias}.*", )); 

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

Безусловно самый лучший вариант в данном случае — это хранение ТВ-значений в самой системной таблице site_content, то есть каждое значение хранится в отдельной колонке этой таблицы.

Если кто думает, что это очередной урок по изъезженной теме CRC, то это не совсем так. Традиционно нас учили расширять имеющиеся классы своими и там дописывать нужные нам колонки (а то и вовсе таблицу собственную прописывать). Но этот путь не оптимальный. Главная проблема здесь — это то, что мы расширяем как-то то класс, но не меняем его самого. Расширения касаются только расширяющего (а не расширяемого) класса, а так же тех расширяющих классов, которые будут расширять наш класс. Запутанно, но сложно проще сказать. Объясню. У нас есть базовые класс modResource. Его расширяют классы modDocument, modWebLink, modSimLink и т.п. Все они наследуют от modResource мапу таблицы. Если мы расширим нашим классом класс modResource, то в нашем классе будут новые колонки которые мы допишем, но их не будет в классе modDocument, так как он не расширяет наш класс. Для того, чтобы информация о новых колонках появилась во всех расширяющих modResource классах, информация эта должна быть в самом классе modResource. Но как это сделать не трогая самих системных файлов?.. На самом деле частично об этом я писал еще более двух лет назад (статью перенес сюда), но только сейчас это реализовал в боевом режиме. Делаем так:

1. Создаем новый компонент, который будет подгружаться как extensionPackage (подробно об этом писал здесь).

2. Создаем новые колонки в таблице site_content через phpMyAdmin или типа того.

3. С помощью CMPGenerator-а генерируем отдельный пакет с мапой таблицы site_content. В этой мапе будет и описание ваших новых колонок и таблиц.

4. Прописываем в вашем пакете в файле metadata.mysql.php данные ваших колонок и индексов (пример такого файла можно увидеть и в нашей сборке ShopModxBox).

К примеру у меня этот файл выглядит примерно так

<?php $custom_fields = array(     "modResource"   => array(         "fields"    => array(             "article_type"  => array(                 "defaultValue"  => NULL,                 "metaData"  => array (                     'dbtype' => 'tinyint',                     'precision' => '3',                     'attributes' => 'unsigned',                     'phptype' => 'integer',                     'null' => true,                     'index' => 'index',                 ),             ),             "image"  => array(                 "defaultValue"  => NULL,                 "metaData"  => array (                   'dbtype' => 'varchar',                   'precision' => '512',                   'phptype' => 'string',                   'null' => false,                 ),             ),         ),                  "indexes"   => array(             'article_type' =>              array (               'alias' => 'article_type',               'primary' => false,               'unique' => false,               'type' => 'BTREE',               'columns' =>                array (                 'article_type' =>                  array (                   'length' => '',                   'collation' => 'A',                   'null' => true,                 ),               ),             ),         ),     ), );  foreach($custom_fields as $class => $class_data){     foreach($class_data['fields'] as $field => $data){         $this->map[$class]['fields'][$field] = $data['defaultValue'];         $this->map[$class]['fieldMeta'][$field] = $data['metaData'];     }          if(!empty($class_data['indexes'])){         foreach($class_data['indexes'] as $index => $data){             $this->map[$class]['indexes'][$index] = $data;         }     } } 

Внимательно его изучите. Он добавляет информацию о двух колонках и одном индексе в таблицу site_content.

Давайте убедимся, что колонки действительно были добавлены. Выполним в консоли этот код:

$o = $modx->newObject('modDocument'); print_r($o->toArray()); 

Увидим вот такой результат:

Array (     [id] =>      [type] => document     [contentType] => text/html     [pagetitle] =>      [longtitle] =>      // Тут еще куча колонок перечислено     // и в конце наши две колонки     [article_type] =>      [image] =>  ) 

Вот теперь мы можем работать с системной таблицей с нашими кастомными полями. К примеру, так можно писать:

$resource = $modx->getObject('modResource', $id); $resource->article_type = $article_type; $resource->save(); 

В таблицу для этого документа будет записано наше значение.

Создание своих колонок и индексов на чистом MODX.

Понятное дело что при таком подходе у нас возникает проблема миграции с такого кастомного сайта на чистый MODX, ведь там в таблицах нет наших кастомных полей и индектов. Но на самом деле это как бы и не проблема совсем. Дело в том, что как мы генерируем мапу из таблиц, так и таблицы, колонки и индексы мы можем создавать из мап-описаний классов. Создать колонку или индекс очень просто:

// Получаем менеджер работы с базой данных $manager = $modx->getManager(); // Создаем колонку  $manager->addField($className, $fieldName); // Создаем индекс $manager->addIndex($className, $fieldName); 

При этом не надо никакие данные колонок и индексов указывать кроме как их названия. Эти данные xPDO получит из нашей мапы и использует при создании описанной колонки или индекса.

Если вы свой компонент соберете в нормальный установочный пакет, то там можете прям прописать скрипт чтобы при установке пакета сразу были созданы в таблицах ваши кастомные колонки и индексы.

Рендеринг ваших кастомных данных в TV-полях при редактировании документов.

Как я и говорил выше, удобство TV-шек заключается в том, что для них созданы различные управляющие элементы (текстовые поля, выпадающие списка, чекбоксы, радиобоксы и т.п.). Плюс к этому в родном редакторе форм можно разграничить права на те или иные ТВ-поля, чтобы кому не покладено не мог видеть/редактировать приватные поля. На самом деле можно, если очень хочется, но все же приватные поля не будут мозолить глаза кому не поподя. И вот как раз эти механизмы и не хотелось бы терять, ибо иначе придется фигачить свои собственные интерфейсы на управление этими данными, а это весьма трудозатратно. Хотелось бы все-таки для редактирования таких данных использовать родной редактор ресурсов. Идеального механизма здесь нет, но боле менее пригодный вариант я отработал. Смысл его заключается в том, чтобы на уровне плагина в момент рендеринга формы редактирования документа подставить TV-поле со своим кастомным значением, а при сохранении документа перехватить данные TV-шки и эти данные сохранить в наши кастомные поля. К сожалению, не получается здесь вклиниться как положено (просто потому что API не позволяет), так что мы не можем повлиять на передаваемые процессору документа данные, из-за чего данные ТВшки все равно будут записаны в таблицу ТВшек, но это не проблема — просто после сохранения документа автоматом подчистим эту табличку и все. Вот пример плагина, срабатывающего на три события (1. рендеринг формы редактирования документа с подстановкой TV-поля и кастомными данными, 2. получение данных и изменение объекта документа перед его сохранением, 3. чистка ненужных данных).

Посмотреть код

<?php /*     OnBeforeDocFormSave     OnDocFormSave     OnResourceTVFormRender */  switch($modx->event->name){                           /*         Рендеринг ТВшек     */     case 'OnResourceTVFormRender':                  $categories = & $scriptProperties['categories'];                  foreach($categories as $c_id => & $category){                          foreach($category['tvs'] as & $tv){                                  /*                     Рендеринг тэгов                 */                 if($tv->id == '1'){                     if($document = $modx->getObject('modResource', $resource)){                         $q = $modx->newQuery('modResourceTag');                         $q->select(array(                             "GROUP_CONCAT(distinct tag_id) as tags",                         ));                         $q->where(array(                             "resource_id" => $document->id,                         ));                         $tags = $modx->getValue($q->prepare());                         $value = str_replace(",", "||", $tags);                         $tv->value = $value;                         $tv->relativeValue = $value;                         $inputForm = $tv->renderInput($document, array('value'=> $tv->value));                         $tv->set('formElement',$inputForm);                     }                 }                                  /*                     Рендеринг картинок                 */                 else if($tv->id == 2){                     if($document = $modx->getObject('modResource', $resource)){                         $tv->value = $document->image;                         $tv->relativeValue = $document->image;                         $inputForm = $tv->renderInput($document, array('value'=> $tv->value));                         $tv->set('formElement',$inputForm);                     }                 }                 /*                     Рендеринг статусов                 */                 else if($tv->id == 12){                     if($document = $modx->getObject('modResource', $resource)){                         $tv->value = $document->article_status;                         $tv->relativeValue = $document->article_status;                         $inputForm = $tv->renderInput($document, array('value'=> $tv->value));                         $tv->set('formElement',$inputForm);                     }                 }             }         }                  break;                   // Перед сохранением документа     case 'OnBeforeDocFormSave':         $resource = & $scriptProperties['resource'];         /*             Тэги.             Перед сохранением документа мы получим все старые              теги и установим им active = 0.             Всем актуальным тегам будет установлено active = 1.             После сохранения документа в событии OnDocFormSave мы удалим все не активные теги         */                   if(isset($resource->tv1)){             $tags = array();             foreach((array)$resource->Tags as $tag){                 $tag->active = 0;                 $tags[$tag->tag_id] = $tag;             }                          // $tags = array();                           if(!empty($resource->tv1)){                 foreach((array)$resource->tv1 as $tv_value){                     if($tv_value){                         if(!empty($tags[$tv_value])){                             $tags[$tv_value]->active = 1;                         }                         else{                             $tags[$tv_value] = $modx->newObject('modResourceTag', array(                                 "tag_id"    => $tv_value,                             ));                         }                     }                 }             }                          $resource->Tags = $tags;                          $tags_ids = array();             foreach($resource->Tags as $tag){                 if($tag->active){                     $tags_ids[] = $tag->tag_id;                 }             }                          $resource->tags = ($tags_ids ? implode(",", $tags_ids) : NULL);         }                           /*             Обрабатываем изображение         */         if(isset($resource->tv2)){             $resource->image = $resource->tv2;         }                           /*             Обрабатываем статусы         */         if(isset($resource->tv12)){             $resource->article_status = $resource->tv12;         }                  break;               /*         Сохранение документа     */     case 'OnDocFormSave':         $resource =& $scriptProperties['resource'];         /*             Удаляем все не активные теги         */         $modx->removeCollection('modResourceTag',array(             'active' => 0,             'resource_id' => $resource->id,         ));                  /*             Удаляем TV-картинки, так как они сохраняются в системную таблицу             Удаляем TV-статусы, так как они сохраняются в системную таблицу         */         $modx->removeCollection('modTemplateVarResource',array(             'tmplvarid:in' => array(                 1,  // Тэги                 2,  // Картинки                 12, // Статусы             ),             'contentid' => $resource->id,         ));                  break;  } 

Благодаря этому плагину кастомные данные рендерятся в форму редактирования документа и обрабатываются при его сохранении.

Итог

Из 225+ тысяч записей в таблице дополнительных полей осталось только 78. Конечно не все ТВшки будут фигачиться в системную таблицу (а только те, что используются для поиска и сортировки), и какие-то данные конечно будут в таблице ТВ-полей, но нагрузка все же серьезно снизилась, а запросы стали попроще.

ссылка на оригинал статьи http://habrahabr.ru/post/253737/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *