Mysql 实现无限级分类
在数据库中存储一棵树,实现无限级分类 ClosureTable
https://segmentfault.com/a/1190000014284076
我也是从别的地方找来的,分析后发现挺不错的,特来分享。
无法发图只能简单处理:
id name pid path
1 新闻 0 0
2 中国新闻 1 0-1
3 美国新闻 1 0-1
4 北京新闻 2 0-1-2
5 指甲哥新闻 3 0-1-3
一条语句搞定:
SELECT id,name,pid,path,concat(path,’-‘,id) AS newpath FROM cates ORDER BY newpath
查询后的结果:
id name pid path newpath
1 新闻 0 0 0-1
2 中国新闻 1 0-1 0-1-2
4 北京新闻 2 0-1-2 0-1-2-4
3 美国新闻 1 0-1 0-1-3
5 指甲哥新闻 3 0-1-3 0-1-3-5
可以看出,结构已经很清晰了。非常巧妙的运用了mysql的排序功能,用 concat() 函数把当前路径与当前的id的连接起来(很想连接字符串),起个别名 newpath ,在排序 newpath,巧的是排序时先比较0,在比较1,在比较2,在比较3…,这样查询的结果是直接按曾级排序好的,接下来只需要在前台循环输出就可以了,“层级关系占位符”也好解决,newpath 的 0-1-2-4 这个就相当于无限极占位符,php输出时要用上面的语句查询出来,在后台处理 每一条数据的 newpath 字段值,先把值分割成数组,在统计这个数组,再在数组里添加一个键count(一组数据),把统计的数组个数放到这个键里。
这种方式,假设我们要查询电脑下的所有后代分类,只需要一条sql语句:
select id,name from category where path like ( select concat(path,'-',id,'%') path from category where id=1 );
优点:查询容易,效率高,path字段可以加索引。
缺点:更新节点关系麻烦,需要更新所有后辈的path字段。
DROP TABLE IF EXISTS `prefix_nodes_paths`;
DROP TABLE IF EXISTS `prefix_nodes`;
CREATE TABLE `prefix_nodes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`order` int(11) DEFAULT NULL,
`name` varchar(128) NOT NULL,
`is_deleted` tinyint(1) DEFAULT '0',
`user_id` int(11) NOT NULL,
`user_type` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`),
KEY `name` (`name`),
CONSTRAINT `prefix_nodes_ibfk1` FOREIGN KEY (`parent_id`) REFERENCES `prefix_nodes` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `prefix_nodes_paths` (
`ancestor_id` int(11) NOT NULL,
`descendant_id` int(11) NOT NULL,
`path_length` int(11) NOT NULL,
PRIMARY KEY (`ancestor_id`,`descendant_id`),
KEY `descendant_id` (`descendant_id`),
CONSTRAINT `prefix_nodes_paths_ibfk_1` FOREIGN KEY (`ancestor_id`) REFERENCES `prefix_nodes` (`id`) ON DELETE CASCADE,
CONSTRAINT `prefix_nodes_paths_ibfk_2` FOREIGN KEY (`descendant_id`) REFERENCES `prefix_nodes` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DELIMITER $$
USE `hierarchy_data`$$
DROP TRIGGER IF EXISTS `trigger_add_node`$$
CREATE
TRIGGER `trigger_add_node` AFTER INSERT ON `prefix_nodes`
FOR EACH ROW BEGIN
CALL p_node_add(NEW.`id`, NEW.`parent_id`);
END;
$$
DELIMITER ;
DELIMITER $$
USE `hierarchy_data`$$
DROP TRIGGER IF EXISTS `trigger_move_node`$$
CREATE TRIGGER `trigger_move_node` AFTER UPDATE ON `prefix_nodes`
FOR EACH ROW BEGIN
IF OLD.`parent_id` != NEW.`parent_id` THEN
-- http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/
-- in the example, when change node D's parent to B.
-- its sql has only D & B. so I think it should currentNode.id& newParent.id
CALL p_node_move(NEW.`id`, NEW.`parent_id`);
END IF;
IF OLD.`is_deleted` != NEW.`is_deleted` THEN
CALL p_node_hide(NEW.`parent_id`, NEW.`is_deleted`);
END IF;
END;
$$
DELIMITER ;
DELIMITER $$
USE `hierarchy_data`$$
DROP PROCEDURE IF EXISTS `p_get_tree`$$
CREATE PROCEDURE `p_get_tree`(
node_id INT UNSIGNED
) COMMENT 'Query all descendants nodes by a node id, return as a result set'
BEGIN
SELECT
node.`id`,
node.`is_deleted`,
node.`parent_id`,
CONCAT(
REPEAT('-', path.`path_length`),
node.`name`
) AS name,
path.`path_length`,
GROUP_CONCAT(
crumbs.`ancestor_id` SEPARATOR ','
) AS breadcrumbs
FROM
`prefix_nodes` AS node
JOIN `prefix_nodes_paths` AS path
ON node.`id` = path.`descendant_id`
JOIN `prefix_nodes_paths` AS crumbs
ON crumbs.`descendant_id` = path.`descendant_id`
WHERE path.`ancestor_id` = `node_id`
AND node.`is_deleted` = 0
GROUP BY node.`id`
ORDER BY breadcrumbs ;
END$$
DELIMITER ;
DELIMITER $$
USE `hierarchy_data`$$
DROP PROCEDURE IF EXISTS `p_node_add`$$
CREATE PROCEDURE `p_node_add`(
param_node_new_id INT UNSIGNED,
param_node_parent_id INT UNSIGNED
)
COMMENT 'Adding new paths prefix_nodes_paths table'
BEGIN
-- Update paths information
INSERT INTO `prefix_nodes_paths` (
`ancestor_id`,
`descendant_id`,
`path_length`
)
SELECT
`ancestor_id`,
`param_node_new_id`,
`path_length` + 1
FROM
`prefix_nodes_paths`
WHERE `descendant_id` = `param_node_parent_id`
UNION
ALL
SELECT
`param_node_new_id`,
`param_node_new_id`,
0 ;
END$$
DELIMITER ;
DELIMITER $$
USE `hierarchy_data`$$
DROP PROCEDURE IF EXISTS `p_node_hide`$$
CREATE PROCEDURE `p_node_hide` (
`node_id` INT UNSIGNED,
`deleted` INT UNSIGNED
) COMMENT 'Delete a node and its descendant nodes(update is_deleted = 1)'
BEGIN
UPDATE
`prefix_nodes` AS d
JOIN `prefix_nodes_paths` AS p
ON d.`id` = p.`descendant_id`
JOIN `prefix_nodes_paths` AS crumbs
ON crumbs.`descendant_id` = p.`descendant_id` SET d.`is_deleted` = deleted
WHERE p.`ancestor_id` = node_id;
END $$
DELIMITER ;
DELIMITER $$
USE `hierarchy_data`$$
DROP PROCEDURE IF EXISTS `p_node_move`$$
CREATE PROCEDURE `p_node_move` (
`node_old_parent_id` INT UNSIGNED,
`node_new_parent_id` INT UNSIGNED
) COMMENT 'Update paths when parent_id column changed'
BEGIN
-- References:
-- http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/
-- The store procedure is used to update paths informations when the value of parent_id columns is changed (when move a node to a new parent)
-- If parent_id has chanaged
-- 1. Delete the paths between moved node and old ancestors
-- 2. Add the paths between moved node and new ancestors
DELETE
a
FROM
`prefix_nodes_paths` AS a
JOIN `prefix_nodes_paths` AS d
ON a.`descendant_id` = d.`descendant_id`
LEFT JOIN `prefix_nodes_paths` AS x
ON x.`ancestor_id` = d.`ancestor_id`
AND x.`descendant_id` = a.`ancestor_id`
WHERE d.`ancestor_id` = `node_old_parent_id`
AND x.`ancestor_id` IS NULL ;
-- Add the node to its new parent
INSERT `prefix_nodes_paths` (
`ancestor_id`,
`descendant_id`,
`path_length`
)
SELECT
supertree.`ancestor_id`,
subtree.`descendant_id`,
supertree.`path_length` + subtree.`path_length` + 1
FROM
`prefix_nodes_paths` AS supertree
JOIN `prefix_nodes_paths` AS subtree
WHERE subtree.`ancestor_id` = `node_old_parent_id`
AND supertree.`descendant_id` = `node_new_parent_id` ;
END$$
DELIMITER ;
INSERT INTO `prefix_nodes` VALUES(1, NULL, NULL, 'ROOT',DEFAULT, 0, 0);
INSERT INTO `prefix_nodes` VALUES(2, 1, NULL, 'C0',DEFAULT, 0, 3);
INSERT INTO `prefix_nodes` VALUES(3, 1, NULL, 'B0',DEFAULT, 0, 2);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A1',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A2',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A3',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A4',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A5',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A6',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A7',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A8',DEFAULT, 0, 1);
INSERT INTO `prefix_nodes` VALUES(NULL, 3, NULL, 'A9',DEFAULT, 0, 1);