У меня есть дерево (вложенные категории), хранящееся следующим образом:
CREATE TABLE `category` (
`category_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`category_name` varchar(100) NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`category_id`),
UNIQUE KEY `category_name_UNIQUE` (`category_name`,`parent_id`),
KEY `fk_category_category1` (`parent_id`,`category_id`),
CONSTRAINT `fk_category_category1` FOREIGN KEY (`parent_id`) REFERENCES `category` (`category_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
Мне нужно передать свой клиентский язык (PHP) информацией об узле (дочерний + родительский), чтобы он мог строить дерево в памяти. Я могу настроить свой PHP-код, но я думаю, что операция была бы намного проще, если бы я мог просто извлекать строки в таком порядке, чтобы все родители стояли перед своими детьми. Я мог бы сделать это, если бы знал уровень для каждого узла:
SELECT category_id, category_name, parent_id
FROM category
ORDER BY level -- No `level` column so far :(
Можете ли вы придумать способ (просмотр, сохраненную подпрограмму или что-то еще ...) для вычисления уровня узла? Я думаю, это нормально, если это не в режиме реального времени, и мне нужно пересчитать его при модификации узла.
Первое обновление: прогресс на данный момент
Я написал эти триггеры, основываясь на отзывах Амаргоша:
DROP TRIGGER IF EXISTS `category_before_insert`;
DELIMITER //
CREATE TRIGGER `category_before_insert` BEFORE INSERT ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET @parent_level = 0;
ELSE
SELECT level INTO @parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = @parent_level+1;
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `category_before_update`;
DELIMITER //
CREATE TRIGGER `category_before_update` BEFORE UPDATE ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET @parent_level = 0;
ELSE
SELECT level INTO @parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = @parent_level+1;
END//
DELIMITER ;
Кажется, он отлично работает для вставок и модификаций. Но это не работает для удалений: MySQL Server не запускает триггеры, когда строки обновляются с внешних ключей ON UPDATE CASCADE
.
Первая очевидная идея - написать новый триггер для удаления; однако триггер в таблице categories
не может изменять другие строки в этой же таблице:
DROP TRIGGER IF EXISTS `category_after_delete`;
DELIMITER //
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
/*
* Raises an error, see below
*/
UPDATE category SET parent_id=NULL
WHERE parent_id = OLD.category_id;
END//
DELIMITER ;
Ошибка:
Ошибка редактирования сетки: ошибка SQL (1442):
Не удается обновить таблицу "категория" в
сохраненная функция / триггер, потому что это
уже используется заявлением, которое
вызвал эту сохраненную функцию / триггер.
Второе обновление: рабочее решение (если не доказано, что неправильно)
Моя первая попытка была довольно разумной, но я нашел проблему, которую не смог решить: когда вы запускаете серию операций из триггера, MySQL не позволяет изменять другие строки из той же таблицы. Поскольку удаление узлов требует корректировки уровня всех потомков, я попал в стену.
В конце я изменил подход, используя код с здесь : вместо того, чтобы исправлять отдельные уровни при изменении узла, у меня есть код для вычисления всех уровней, и я запускаю его при каждом редактировании. Поскольку это медленный расчет и выборка данных требует очень сложного запроса, я кеширую его в таблицу. В моем случае это приемлемое решение, так как издания должны быть редкими.
1. Новая таблица для кэшированных уровней:
CREATE TABLE `category_level` (
`category_id` int(10) NOT NULL,
`parent_id` int(10) DEFAULT NULL, -- Not really necesary
`level` int(10) NOT NULL,
PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
2. Вспомогательная функция для расчета уровней
Если я действительно понял, как это работает, он сам по себе не возвращает ничего полезного. Вместо этого он сохраняет данные в переменных сеанса.
CREATE FUNCTION `category_connect_by_parent_eq_prior_id`(`value` INT) RETURNS int(10)
READS SQL DATA
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _next INT;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET @category_id = NULL;
SET _parent = @category_id;
SET _id = -1;
IF @category_id IS NULL THEN
RETURN NULL;
END IF;
LOOP
SELECT MIN(category_id)
INTO @category_id
FROM category
WHERE COALESCE(parent_id, 0) = _parent
AND category_id > _id;
IF @category_id IS NOT NULL OR _parent = @start_with THEN
SET @level = @level + 1;
RETURN @category_id;
END IF;
SET @level := @level - 1;
SELECT category_id, COALESCE(parent_id, 0)
INTO _id, _parent
FROM category
WHERE category_id = _parent;
END LOOP;
END
3. Порядок запуска процесса пересчета
Он в основном инкапсулирует сложный запрос, который извлекает уровни с помощью вспомогательной функции.
CREATE PROCEDURE `update_category_level`()
SQL SECURITY INVOKER
BEGIN
DELETE FROM category_level;
INSERT INTO category_level (category_id, parent_id, level)
SELECT hi.category_id, parent_id, level
FROM (
SELECT category_connect_by_parent_eq_prior_id(category_id) AS category_id, @level AS level
FROM (
SELECT @start_with := 0,
@category_id := @start_with,
@level := 0
) vars, category
WHERE @category_id IS NOT NULL
) ho
JOIN category hi ON hi.category_id = ho.category_id;
END
4. Триггеры для поддержания кеш-таблицы в актуальном состоянии
CREATE TRIGGER `category_after_insert` AFTER INSERT ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_update` AFTER UPDATE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
5. Известные проблемы
- Это довольно неоптимально, если узлы часто меняются.
- MySQL не разрешает транзакции или блокировку таблиц в триггерах и процедурах. Вы должны позаботиться об этих деталях при редактировании узлов.