Оптимизируем K2. Дерево категорий. Сокращаем количество запросов к базе данных
Здесь пойдёт речь о k2 версии 2.5.4. K2 для Joomla – одно из самых сильных и масштабируемых расширений. CCK K2 с лёгкостью выдерживает нагрузку в десятки тысяч статей даже на обыкновенном хостинге. При этом K2 обладает очень сильной кэширующей системой!
В кэше любая категория с новостями/товарами производит всего 16 — 18 запросов к базе данных на страницу вместе с запросами Joomla, что, по сути, сравнимо с Joomla без использования k2. Но, не всегда есть возможность использовать кэш. А также кэш не будет действовать после авторизации (входа посетителя на сайт).
Примечание автора: тоесть, из-за формирования дерева категорий K2 количество запросов к базе данных на Вашем сайте с отключенным кэшем или для авторизированных посетителей (после входа на сайт) будет равно (количество категорий * 3). В этой же статье мы сведём количество запросов к базе данных на формирование всего дерева категорий к одному.
Путём мониторинга запросов к базе данных было найдено «узкое место». Первым слабым звеном здесь является показ количества материалов напротив категорий (+1 запрос на каждую категорию).
Убираем счётчик материалов в навигации категорий K2
Заходим в панель администрирования – в верхнем меню расширения — менеджер модулей – ищем модуль mod_k2_tools, отвечающий за вывод навигации по категориям, щелкаем по его названию (в параметрах модуля в выпадающем списке «Выберите функциональность модуля» должно быть отмечено «Категории (меню)»), переходим в правой части экрана к «Категории (меню) Настройки», отмечаем флаг «Счётчик материалов» – «скрыть», в верхнем правом углу экрана нажимаем «Сохранить».
Переписываем mod_k2_tools
Открываем в директории Вашего сайта файл: /modules/mod_k2_tools/helper.php (в конце статьи Вы можете найти модифицированный файл helper.php для версии k2 2.5.4, и распаковать в папку: /modules/mod_k2_tools/, заменив оригинальный helper.php).
Ищем по поиску function hasChildren($id) { (приблизительно 337 строка – начало, 370 строка – конец для версии k2 2.5.4), заменяем всю функцию hasChildren на:
function hasChildren($id) {
global $k2_categories;
$mainframe = &JFactory::getApplication();
$user = &JFactory::getUser();
$aid = (int) $user->get('aid');
$id = (int) $id;
$db = &JFactory::getDBO();
if(!is_array($k2_categories)){
$query = "SELECT * FROM #__k2_categories WHERE published=1 AND trash=0 ";
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$db->setQuery($query);
$rows = $db->loadResult();
$k2_categories = $rows;
if ($db->getErrorNum()) {
echo $db->stderr();
return false;
}
}
$rows = array();
foreach ($k2_categories as $category){
if($category->parent == $id){
$rows[] = $category;
break;
}
}
if (sizeof($rows)) {
return true;
} else {
return false;
}
}
Ищем по поиску function treerecurse (&$params, $id = 0, $level = 0, $begin = false) { : (приблизительно 395 строка — начало, 491 строка — конец для версии k2 2.5.4):
Заменяем всю функцию treerecurse на:
function treerecurse(&$params, $id = 0, $level = 0, $begin = false) {
static $output;
global $k2_categories;
if ($begin) {
$output = '';
}
$mainframe = &JFactory::getApplication();
$root_id = (int) $params->get('root_id');
$end_level = $params->get('end_level', NULL);
$id = (int) $id;
$catid = JRequest::getInt('id');
$option = JRequest::getCmd('option');
$view = JRequest::getCmd('view');
$user = &JFactory::getUser();
$aid = (int) $user->get('aid');
$db = &JFactory::getDBO();
switch ($params->get('categoriesListOrdering')) {
case 'alpha':
$orderby = 'name';
break;
case 'ralpha':
$orderby = 'name DESC';
break;
case 'order':
$orderby = 'ordering';
break;
case 'reversedefault':
$orderby = 'id DESC';
break;
default:
$orderby = 'id ASC';
break;
}
$rows = array();
if(!is_array($k2_categories)){
$query = "SELECT * FROM #__k2_categories WHERE published=1 AND trash=0 ";
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$query .= " ORDER BY {$orderby}";
$db->setQuery($query);
$k2_categories = $db->loadObjectList();
}
if (($root_id != 0) && ($level == 0)) {
foreach ($k2_categories as $category){
if($category->parent == $root_id){
$rows[] = $category;
}
}
} else {
foreach ($k2_categories as $category){
if($category->parent == $id){
$rows[] = $category;
}
}
}
if ($db->getErrorNum()) {
echo $db->stderr();
return false;
}
if ($level < intval($end_level) || is_null($end_level)) {
$output .= '<ul class="level'.$level.'">';
foreach ($rows as $row) {
if ($params->get('categoriesListItemsCounter')) {
$row->numOfItems = ' ('.modK2ToolsHelper::countCategoryItems($row->id).')';
} else {
$row->numOfItems = '';
}
if (($option == 'com_k2') && ($view == 'itemlist') && ($catid == $row->id)) {
$active = ' class="activeCategory"';
} else {
$active = '';
}
if (modK2ToolsHelper::hasChildren($row->id)) {
$output .= '<li'.$active.'><a href="'.urldecode(JRoute::_(K2HelperRoute::getCategoryRoute($row->id.':'.urlencode($row->alias)))).'"><span class="catTitle">'.$row->name.'</span><span class="catCounter">'.$row->numOfItems.'</span></a>';
modK2ToolsHelper::treerecurse($params, $row->id, $level + 1);
$output .= '</li>';
} else {
$output .= '<li'.$active.'><a href="'.urldecode(JRoute::_(K2HelperRoute::getCategoryRoute($row->id.':'.urlencode($row->alias)))).'"><span class="catTitle">'.$row->name.'</span><span class="catCounter">'.$row->numOfItems.'</span></a></li>';
}
}
$output .= '</ul>';
}
return $output;
}
Примечание автора: мы полностью сохраняем функциональность модуля, не «урезая» никаких возможностей. Просто здесь другой подход к формированию дерева категорий.
Итак, что же мы изменили? Оригинальная функция hasChildren: (задачей которой является проверка, есть ли у данной категории дочерние категории? Если есть – она запускает функцию treerecurse для формирования дерева дочерних категорий):
$query = "SELECT * FROM #__k2_categories WHERE parent={$id} AND published=1 AND trash=0 ";
Данный запрос выбирает все категории, для которых указанная категория ($id) является родительской.
if (count($rows)) {
return true;
} else {
return false;
}
Если количество категорий больше «0», тогда он возвращает true, иначе false. Видоизменяем код,
объявляем глобальную переменную:
global $k2_categories;
Сразу после объявления функций
Если эта переменная ещё не является массивом (а такое может быть только один раз, после её объявления): выбираем все опубликованные категории, которые «не находятся в корзине» одним запросом к базе данных, и присваиваем полученный массив глобальной переменной $k2_categories (итого, к выборке массива категорий из базы данных мы обратимся всего лишь один раз – 1 запрос к базе данных, в дальнейшем же мы будем работать с массивом категорий напрямую):
if(!is_array($k2_categories)){
$query = "SELECT * FROM #__k2_categories WHERE published=1 AND trash=0 ";
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$db->setQuery($query);
$rows = $db->loadResult();
$k2_categories = $rows;
if ($db->getErrorNum()) {
echo $db->stderr();
return false;
}
}
Далее в цикле обходим наш массив и выбираем дочерние категории для родительской:
$rows = array();
foreach ($k2_categories as $category){
if($category->parent == $id){
$rows[] = $category;
break;
}
}
if (sizeof($rows)) {
return true;
} else {
return false;
}
Если есть хотя бы одна дочерняя категория: разрешаем функции treerecurse формирование списка дочерних категорий
function treerecurse(&$params, $id = 0, $level = 0, $begin = false) {
Данная функция «формирует» дерево категорий, и отображает дочерние категории для родительского текущего уровня, для этого на каждой подкатегории она проверяет наличие дочерних категорий, вызывая функцию hasChild (которую мы уже переписали), и выбирает список категорий текущего уровня.
if (($root_id != 0) && ($level == 0)) {
$query = "SELECT * FROM #__k2_categories WHERE parent={$root_id} AND published=1 AND trash=0 ";
} else {
$query = "SELECT * FROM #__k2_categories WHERE parent={$id} AND published=1 AND trash=0 ";
}
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$query .= " ORDER BY {$orderby}";
$db->setQuery($query);
$rows = $db->loadObjectList();
if ($db->getErrorNum()) {
echo $db->stderr();
return false;
}
Здесь функция добавляет по одному запросу к базе данных на текущую категорию + 1 запрос на проверку дочерних категорий (который мы уже исключили). Добавляем нашу глобальную переменную, после определения функции:
global $k2_categories;
Запрос к базе данных:
if (($root_id != 0) && ($level == 0)) {
$query = "SELECT * FROM #__k2_categories WHERE parent={$root_id} AND published=1 AND trash=0 ";
} else {
$query = "SELECT * FROM #__k2_categories WHERE parent={$id} AND published=1 AND trash=0 ";
}
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$query .= " ORDER BY {$orderby}";
$db->setQuery($query);
$rows = $db->loadObjectList();
if ($db->getErrorNum()) {
echo $db->stderr();
return false;
}
Заменяем на код:
if(!is_array($k2_categories)){
$query = "SELECT * FROM #__k2_categories WHERE published=1 AND trash=0 ";
if(K2_JVERSION=='16'){
$query .= " AND access IN(".implode(',', $user->authorisedLevels()).") ";
if($mainframe->getLanguageFilter()) {
$languageTag = JFactory::getLanguage()->getTag();
$query .= " AND language IN (".$db->Quote($languageTag).", ".$db->Quote('*').") ";
}
}
else {
$query .= " AND access <= {$aid}";
}
$query .= " ORDER BY {$orderby}";
$db->setQuery($query);
$k2_categories = $db->loadObjectList();
}
В котором мы проверяем, является ли глобальная переменная $k2_categories массивом, если нет, тогда выбираем все категории, которые опубликованы и не находятся в корзине, и присваиваем полученный массив нашей глобальной переменной.
После чего в цикле мы «выбираем» категории нужного подуровня (начиная с указанной в настройках модуля категории):
if (($root_id != 0) && ($level == 0)) {
foreach ($k2_categories as $category){
if($category->parent == $root_id){
$rows[] = $category;
}
}
} else {
foreach ($k2_categories as $category){
if($category->parent == $id){
$rows[] = $category;
}
}
}
Далее функция treerecurse остаётся неизменной. Как итог – мы добились сокращения запросов к базе данных при формировании списка категорий с трёх на одну категорию до одного на формирование полного списка навигации.