Оптимизируем 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 остаётся неизменной. Как итог – мы добились сокращения запросов к базе данных при формировании списка категорий с трёх на одну категорию до одного на формирование полного списка навигации.