Álv*_*lez 6 mysql sql innodb hierarchical-data mysql-error-1442
我有一个树(嵌套类别)存储如下:
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
Run Code Online (Sandbox Code Playgroud)
我需要用节点信息(子+父)提供我的客户端语言(PHP),以便它可以在内存中构建树.我可以调整我的PHP代码,但我认为如果我能按照所有父母在他们的孩子面前的顺序检索行,那么操作会更简单.如果我知道每个节点的级别,我就可以这样做:
SELECT category_id, category_name, parent_id
FROM category
ORDER BY level -- No `level` column so far :(
Run Code Online (Sandbox Code Playgroud)
你能想出一种方法(视图,存储例程或其他......)来计算节点级别吗?我想如果它不是实时的,我可以在节点修改时重新计算它.
我根据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 ;
Run Code Online (Sandbox Code Playgroud)
它似乎适用于插入和修改.但它不适用于删除:当从ON UPDATE CASCADE外键更新行时,MySQL Server不会启动触发器.
第一个明显的想法是编写一个新的删除触发器; 但是,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 ;
Run Code Online (Sandbox Code Playgroud)
错误:
网格编辑错误:SQL错误(1442):无法更新存储的函数/触发器中的表'category',因为它已被调用此存储函数/触发器的语句使用.
我的第一次尝试非常明智,但我发现了一个无法解决的问题:当你从触发器启动一系列操作时,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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
5.已知问题