在树中查找节点级别

时间:2010-06-09 08:33:39

标签: 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

我需要用节点信息(子+父)提供我的客户端语言(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 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 ;

错误:

  

网格编辑错误:SQL错误(1442):   无法更新表'类别'   存储函数/触发器,因为它   已经被声明使用过了   调用了这个存储的函数/触发器。

第二次更新:工作解决方案(除非证明错误)

我的第一次尝试非常明智,但我发现了一个无法解决的问题:当您从触发器启动一系列操作时,MySQL将不允许从同一个表中更改其他行。由于节点删除需要调整所有后代的级别,因此我遇到了障碍。

最后,我使用here中的代码更改了方法:而不是在节点更改时更正个别级别,我有代码来计算所有级别,并在每次编辑时触发它。由于计算速度慢而且获取数据需要非常复杂的查询,因此我将其缓存到表中。在我的情况下,这是一个可接受的解决方案,因为版本应该是罕见的。

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不允许触发器和过程中的事务或表锁定。您必须在编辑节点的地方处理这些细节。

3 个答案:

答案 0 :(得分:3)

这里有一篇关于Hierarchical Queries in MySQL的优秀系列文章,其中包括如何识别层次结构,叶子节点,层次结构中的循环等。

答案 1 :(得分:2)

如果没有任何循环(如果它始终是树而不是图形),则默认情况下可以将level字段设置为零(最顶层)并存储更新parent_id时将级别更新为(父级别+ 1)的过程。

CREATE TRIGGER setLevelBeforeInsert BEFORE INSERT ON category
FOR EACH ROW
BEGIN
IF NEW.parent_id IS NOT NULL THEN
SELECT level INTO @pLevel FROM category WHERE id = NEW.parent_id;
SET NEW.level = @pLevel + 1;
ELSE 
SET NEW.level = 0;
END IF;
END;

答案 2 :(得分:0)

  到目前为止,

没有level列:(

嗯*耸肩*
我只是手动制作了这个等级字段 比如物化路径,在插入后只有一次更新而没有所有这些花哨的触发器 例如

的第3级的000000100000210000022字段
  

所以它可以在内存中构建树。

如果要将整个表格导入PHP,我认为这里没有问题。一个小的递归函数可以为你提供嵌套的数组树。

  

我可以调整我的PHP代码,但我认为操作会更简单

好吧,好吧 你到目前为止所获得的代码在我看来并不“简单”:)