我有一棵树(嵌套类别)存储如下:
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 :(
你能想出一种方法(视图、存储的例程或其他什么……)来计算节点级别吗?我想如果它不是实时的也没关系,我需要在节点修改时重新计算它。
第一次更新:到目前为止的进展
我根据 Amarghosh 的反馈编写了这些触发器:
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 ;
它似乎适用于插入和修改。但它不适用于删除:当从ON UPDATE CASCADE
外键更新行时,MySQL 服务器不会启动触发器。
第一个明显的想法是写一个新的删除触发器;categories
但是,不允许table 上的触发器修改同一个 table 上的其他行:
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 不允许触发器和过程中的事务或表锁定。在编辑节点时,您必须注意这些细节。