不管你是做前端还是后端的开发,那我相信树形结构的需求一定有遇到过,特别是管理平台类型的项目,一般都会有一个树形结构的菜单栏,再比如说,公司组织架构,层级关系、归属关系等等需求,本质上都是树形结构的一种体现;
遇到这种需求,最常见也最容易想到的设计思路就是:父子关系的方式,子项通过一个字段来保存他的父ID,然后通过递归的方式得到层级关系;
前几天,技术交流群里面有小伙伴在问,实际的业务中,树形结构的数据太多、层级还深,查询过程一顿递归之后,性能表现得比较差,问有没有什么好的设计思路,让查询、统计更加的便捷高效;
今天就给大家介绍一种新的树形结构设计方案:改进后的先序树方式,在查询、统计方面的优势,要远大于父子关系的递归设计方案;
本文就来详细的讲解并对比一下两个方案的实现方式以及优缺点。
文章目录:
对于树形结构的需求,不管是采用什么方式,要处理的问题都是差不多的,下面先列举一下树形结构常见的问题点有哪些:
- 节点的增删改
- 是否存在子节点(叶子节点)
- 查询出所有的子节点
- 查询所有的节点
- 查询所有的子孙节点
- 父节点查询
- 祖先节点查询
- 统计所有子孙部门的数量
针对上面的这些问题,就以一个简单的公司组织架构示例,一起来看看,两种方案都是如何实现和解决的?
本文所有的示例都是采用的MySQL+Java实现,核心思路都类似,实际使用,可根据语言特性以及自己习惯的方式调整即可。
1父子关系方案
父子关系,顾名思义,就是当前节点只关注自己的父节点是谁,并将其保存起来即可,查询我的子节点有哪些,只需要全局找到所有父ID是和我的ID一致的项;
如下图所示:
方案特点
- 优点
- 方案简单易懂
- 数据结构简单清晰
- 层级直观、鲜明
- 易维护层级关系只需要关注自己的父ID,所以在添加、修改的时候,一旦关系发生变化,调整对应的父ID即可。
- 缺点
- 查找麻烦、统计麻烦根据当前节点的数据,只能获取到子节点的数据,一旦查询、统计超出父子范围,就只能通过递归逐层查找了;
示例
根据上面的图示示例,与其对应的表结构如下:
IDdep_name(部门名称)level(层级)parent_id(父ID)1董事会102总经理213董事会秘书214产品部325行政总监326设计部447技术部448财务部459行政部4510客户端5711服务端57
SQL脚本:
DROP TABLE IF EXISTS `department_info`;
CREATE TABLE `department_info` (
`id` int(11) NOT NULL,
`dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
`level` int(11) NULL DEFAULT NULL,
`parent_id` int(11) NULL DEFAULT NULL COMMENT '父ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `department_info` VALUES (1, '董事会', 1, 0);
INSERT INTO `department_info` VALUES (2, '总经理', 2, 1);
INSERT INTO `department_info` VALUES (3, '董事会秘书', 2, 1);
INSERT INTO `department_info` VALUES (4, '产品部', 3, 2);
INSERT INTO `department_info` VALUES (5, '行政总监', 3, 2);
INSERT INTO `department_info` VALUES (6, '设计部', 4, 4);
INSERT INTO `department_info` VALUES (7, '技术部', 4, 4);
INSERT INTO `department_info` VALUES (8, '财务部', 4, 5);
INSERT INTO `department_info` VALUES (9, '行政部', 4, 5);
INSERT INTO `department_info` VALUES (10, '客户端', 5, 7);
INSERT INTO `department_info` VALUES (11, '服务端', 5, 7);
函数的创建
由于父子节点的查询,需要依赖递归,为了查询方便,这里创建两个函数:
- 递归查询子孙节点ID的函数DROP FUNCTION IF EXISTS queryChildrenDepInfo;
DELIMITER ;;CREATE FUNCTION queryChildrenDepInfo(dep_id INT)
RETURNS VARCHAR(4000)
BEGIN
DECLARE sTemp VARCHAR(4000);
DECLARE sTempChd VARCHAR(4000);
SET sTemp='$';
SET sTempChd = CAST(dep_id AS CHAR);
WHILE sTempChd IS NOT NULL DO
SET sTemp= CONCAT(sTemp,',',sTempChd);
SELECT GROUP_CONCAT(id) INTO sTempChd FROM department_info WHERE FIND_IN_SET(parent_id,sTempChd)>0;
END WHILE;
RETURN sTemp;END
;;DELIMITER ;测试:查询技术部下的所有重要节点?SELECT queryChildrenDepInfo(4);
SELECT * FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(4)); - 递归查询祖先节点ID的函数DROP FUNCTION IF EXISTS queryParentDepInfo;
DELIMITER;;CREATE FUNCTION queryParentDepInfo(dep_id INT)
RETURNS VARCHAR(4000)
BEGIN
DECLARE sTemp VARCHAR(4000);
DECLARE sTempChd VARCHAR(4000);
SET sTemp='$';
SET sTempChd = CAST(dep_id AS CHAR);
SET sTemp = CONCAT(sTemp,',',sTempChd);
SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
WHILE sTempChd <> 0 DO
SET sTemp = CONCAT(sTemp,',',sTempChd);
SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
END WHILE;
RETURN sTemp;END
;;DELIMITER ;测试:查询技术部所有的祖先节点?SELECT queryParentDepInfo(7);
SELECT * FROM department_info WHERE FIND_IN_SET(id,queryParentDepInfo(7));
节点的增删改
- 增加节点比如要在技术部下添加一个测试部门INSERT INTO department_info(`id`, `dep_name`, `level`, `parent_id`) VALUES (12, '测试部', 5, 7);
- 修改节点比如:将测试部(ID = 12)提出来,放到产品部(ID = 4)下;就只需要将测试部对应的父节点ID修改为4即可SET @id = 12;
SET @pid = 4;
UPDATE department_info SET `parent_id` = @pid WHERE `id` = @id; - 删除节点删除相比于添加修改,情况就会特殊一些,如果删除的节点存在子节点,意味着子节点也需要同步删除掉;因此这里就需要用到上面创建的递归查询子孙节点ID的函数(queryChildrenDepInfo)比如:删除技术部门;DELETE FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(7));
是否存在子节点(叶子节点)
在该方案下,要想判断是否是叶子节点,有两种实现方式:
- 统计当前节点以及子孙节点的数量递归查询所有子节点的ID,并统计数量,由于函数查询包含了节点自身,所以这里使用了COUNT(*)-1来计算子节点的数量,如果等于0就是叶子节点,大于0说明不是叶子节点;-- 查看设计部(ID=6)是不是叶子节点
SET @id = 6;
-- 由于数量包含了自身,由于统计的是子节点的数量,所以这里需要-1将自己去掉
SELECT COUNT(*)-1 FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(@id)); - 添加叶子节点的标记在表中添加一个isLeaf字段,当节点增删改操作的