Оптимизируем K2. Дерево категорий. Сокращаем количество запросов к базе данных

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

Комментарии
Adblock detector