当前位置: 首页 > news >正文

【设计模式】组合模式实现部门树实践

1.前言

几乎在每一个系统的开发过程中,都会遇到一些树状结构的开发需求,例如:组织机构树,部门树,菜单树等。只要是需要开发这种树状结构的需求,我们都可以使用组合模式来完成。

本篇将结合组合模式与Mysql实现一个部门树,完成其增删改和树形结构的组装。

2.组合模式

组合模式是一种结构型设计模式,它允许我们将对象组合成树形结构来表现部分-整体的层次结构。以部门树为例,我们可以将上级部门与下级部门组合起来,形成一个单边树,用代码来描述的话,就是这个样子的:

public class DeptNode {private List<DeptNode> children = new ArrayList<>();
}

提供一个部门节点类,里面会有一个集合,用于保存当前部门的下级部门,同理在children这个集合中的部门节点,也可能会有它的下级部门节点。

当然,这不是实现组合模式的唯一方式,还有其他复杂一点方式,会区分不同的节点类型,是根节点、分支节点、还是叶子节点等。这里之所以做这种简单的设计,是因为我们的树状结构的数据一般都会交给前端去做渲染,在很多前端的组件库中,就是用这种简单的方式来组织树的,例如在Element-UI中的树状结构:
在这里插入图片描述

3.实现方式

3.1.数据结构设计

先看数据库的设计,数据库必要的字段比较简单,直接看一下建表的sql:

create table dept
(id        bigint auto_increment comment '部门id'primary key,parent_id bigint       null comment '上级部门id',name      varchar(200) null comment '部门名称',tree_path varchar(255) null comment '树路径'
)

idparent_id很好理解,主要是用来维护部门的上下级关系,name不解释,tree_path这个字段其实不是必须要的,没有它也可以实现部门树,但是加上这个path之后,可以比较方便的查询子树。


PO对象与数据库字段保持一致,这里就不过多赘述,代码中需要返回给前端的树对象要修改一下字段名,name->label

@Getter
@Setter
public class DeptNode {private List<DeptNode> children = new ArrayList<>();private Long id;private Long parentId;private String label;private String treePath;
}

3.2.数据新增

由于是自增主键,数据的新增需要再保存之后获取到主键id,再更新treePath
这里为了方便,我用了dept对象直接透传,使用的是mybatis-plus操作数据库,可以替换成自己喜欢的ORM。

@Service("deptService")
public class DeptServiceImpl extends ServiceImpl<DeptDao, Dept> implements DeptService {@Override@Transactional(rollbackFor = Exception.class)public void insert(Dept dept) {// 如果有上级部门id,则获取上级机构Dept parentDept = null;if (dept.getParentId() != null) {parentDept = this.getById(dept.getParentId());// 上级机构不能为空if (parentDept == null) {throw new RuntimeException("上级机构不存在");}}// MybatisPlus新增后可以获取主键this.save(dept);// 更新树路径if (parentDept != null) {dept.setTreePath(parentDept.getTreePath() + dept.getId() + "/");} else {dept.setTreePath("/" + dept.getId() + "/");}this.updateById(dept);}
}

3.2.数据更新

数据更新需要注意两个点:

  • 新的上级部门不能是自己,也不能是自己的子部门(避免成环)。
  • 更新树路径之后,树路径上的所有子部门都需要更新树路径。
@Override
@Transactional(rollbackFor = Exception.class)
public void update(Dept dept) {Dept newParentDept = null;if (dept.getParentId() != null) {newParentDept = this.getById(dept.getParentId());if (newParentDept == null) {throw new RuntimeException("上级部门不存在");}if (newParentDept.getTreePath().contains("/" + dept.getId() + "/")) {throw new RuntimeException("上级部门不能是自己或子部门");}}this.updateById(dept);// 组装新的树路径String newTreePath = (newParentDept == null ? "" : newParentDept.getTreePath()) + dept.getId() + "/"; + dept.getId() + "/";// 获取原有的树路径String oldTreePath = this.getById(dept.getId()).getTreePath();// 获取所有子部门(循环更新也可以替换为使用Mysql的replace函数批量更新)LambdaQueryWrapper<Dept> queryWrapper = new LambdaQueryWrapper<>();// likeRight表示右模糊查询,即以oldTreePath开头的queryWrapper.likeRight(Dept::getTreePath, oldTreePath);this.list(queryWrapper).forEach(childDept -> {// 更新子部门的树路径childDept.setTreePath(childDept.getTreePath().replace(oldTreePath, newTreePath));this.updateById(childDept);});
}

上面的循环更新在数据量不大的时候可以这么做,如果量较大的话,推荐使用mysql中的replace函数替换:

update dept set tree_path = replace(tree_path,'旧路径','新路径')
where tree_path like '旧路径%'

sql中的旧路径新路径替换为上面代码中获取到的路径即可。

3.4.部门树组装

部门树组装只需要把需要组装的部门列表查询出来,然后根据parent_id的关联关系组装数据即可。这里tree_path就可以派上用场了,如果只有parent_id的话,要么必须全量查询所有的部门再过滤,要么需要根据parent_id做递归查询,而通过tree_path可以直接做右模糊查询,查询到的部门都是需要的部门。

我们可以在接口中接收一个部门的id,把这个部门作为部门子树的根节点:

@Override
public List<DeptNode> tree(Long id) {// 传入了主键id,则通过主键id对于treePath做右模糊查询,没有传入主键id,则查询所有List<Dept> list;if (id != null) {Dept baseDept = this.getById(id);list = this.list(new LambdaQueryWrapper<Dept>().likeRight(Dept::getTreePath, baseDept.getTreePath()));} else {list = this.list();}// 将Dept转换为DeptNodeList<DeptNode> deptNodes = new ArrayList<>();for (Dept dept : list) {DeptNode deptNode = BeanUtil.copyProperties(dept, DeptNode.class);deptNode.setLabel(dept.getName());deptNodes.add(deptNode);}// 循环遍历,将子节点放入父节点的children中for (DeptNode node : deptNodes) {deptNodes.stream().filter(item -> node.getId().equals(item.getParentId())).forEach(item -> {if (node.getChildren() == null) {node.setChildren(CollUtil.newArrayList(item));} else {node.getChildren().add(item);}});}// 返回根节点return deptNodes.stream().filter(item -> item.getParentId() == null || item.getId().equals(id)).collect(Collectors.toList());
}

4.测试

通过一个Controller接口发起测试:

@RestController
@RequestMapping("dept")
public class DeptController {@Resourceprivate DeptService deptService;@PostMapping("insert")public void insert(@RequestBody @Valid Dept dept) {this.deptService.insert(dept);}@PostMapping("update")public void update(@RequestBody @Valid Dept dept) {this.deptService.update(dept);}@PostMapping("/tree")public List<DeptNode> tree(Long id) {return this.deptService.tree(id);}
}

4.1.部门新增

按照下面的请求参数顺序发起insert请求,为了验证的方便,这里的部门加了数字后缀:

{"parentId": null,"name": "根部门"
}
{"parentId": 1,"name": "一级部门-1"
}
{"parentId": 1,"name": "一级部门-2"
}
{"parentId": 2,"name": "二级部门-1-1"
}
{"parentId": 3,"name": "二级部门-2-1"
}
{"parentId": 5,"name": "三级部门-2-1-1"
}
{"parentId": 5,"name": "三级部门-2-1-2"
}

执行后数据的结果如下,我们可以看到tree_path已经正常添加好了:
在这里插入图片描述
通过tree接口,不传id获取到的树结构如下,按照上面说的部门后缀进行对比验证,可以看出部门树已经正确组装了。

[{"children": [{"children": [{"children": [],"id": 4,"parentId": 2,"label": "二级部门-1-1","treePath": "/1/2/4/"}],"id": 2,"parentId": 1,"label": "一级部门-1","treePath": "/1/2/"},{"children": [{"children": [{"children": [],"id": 6,"parentId": 5,"label": "三级部门-2-1-1","treePath": "/1/3/5/6/"},{"children": [],"id": 7,"parentId": 5,"label": "三级部门-2-1-2","treePath": "/1/3/5/7/"}],"id": 5,"parentId": 3,"label": "二级部门-2-1","treePath": "/1/3/5/"}],"id": 3,"parentId": 1,"label": "一级部门-2","treePath": "/1/3/"}],"id": 1,"parentId": null,"label": "根部门","treePath": "/1/"}
]

4.2.部门修改

假设现在我想把二级部门-2-1直接挂接到根部门下,则两个三级部门也会跟着一起迁移,尝试一下做这个修改,请求参数如下:

{"id": 5,"parentId": null,"name": "二级部门-2-1(改)"
}

执行后,数据库的结果如下,tree_path中间的/3/已经去掉了:
在这里插入图片描述

4.3.子树查询

传入二级部门-2-1(改)的id,查询子树,期望可以返回三个部门,一个父部门,两个子部门,请求tree接口的结果与期望相符:

[{"children": [{"children": [],"id": 6,"parentId": 5,"label": "三级部门-2-1-1","treePath": "/1/5/6/"},{"children": [],"id": 7,"parentId": 5,"label": "三级部门-2-1-2","treePath": "/1/5/7/"}],"id": 5,"parentId": 1,"label": "二级部门-2-1(改)","treePath": "/1/5/"}
]

5.结语

通过组合模式加上一点数据库的设计,可以实现大部分常规的树状结构的需求,希望对大家能有所帮助。

相关文章:

【设计模式】组合模式实现部门树实践

1.前言 几乎在每一个系统的开发过程中&#xff0c;都会遇到一些树状结构的开发需求&#xff0c;例如&#xff1a;组织机构树&#xff0c;部门树&#xff0c;菜单树等。只要是需要开发这种树状结构的需求&#xff0c;我们都可以使用组合模式来完成。 本篇将结合组合模式与Mysq…...

恒林家居引入纷享销客CRM系统,领跑家居行业营销数字化进程

近日&#xff0c;恒林家居股份有限公司&#xff08;&#xff08;股票代码&#xff1a;603661以下简称为“恒林家居”&#xff09;携手纷享销客在湖州召开了CRM项目启动会。双方领导及核心项目人员齐聚一堂&#xff0c;展开了深度交流并达成了重要共识。 作为家居行业的领军企业…...

多线程-锁的种类

1 作用 Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性&#xff0c;我们通常需要在使用对象或者方法之前加锁&#xff0c;这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会…...

Hive 和 HDFS、MySQL 之间的关系

文章目录 HiveHDFSMySQL三者的关系 Hive、MySQL 和 HDFS 是三个不同的数据存储和处理系统&#xff0c;它们在大数据生态系统中扮演不同的角色&#xff0c;但可以协同工作以支持数据管理和分析任务。 Hive Hive 是一个基于 Hadoop 生态系统的数据仓库工具&#xff0c;用于管理和…...

【面试题】如何实现数组去重的?有几种方式?

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 【国庆头像】- 国庆爱国 程序员头像&#xff01;总有一款适合你&#xff01; 1. 方法一&#xff1a;利用两层循环数组的splice方法 通过两层循环对数组…...

使用TCP方式拉取Canal数据

1 Canal对接Kafka联调 1.1 配置修改 canal.properties 修改 zk&#xff1a; canal.zkServers 10.51.50.219:2181instance.properties 开启配置项&#xff1a; canal.mq.dynamicTopic 是 Canal 的 MQ 动态 Topic 配置项&#xff1a; test_javaedge_01 是kafka 的 topicte…...

Docker安装mysql实战说明

安装前准备 在安装MySQL之前&#xff0c;你需要确保已经正确安装和配置了Docker&#xff0c;可以通过以下命令检查Docker是否已正确安装&#xff1a; docker --version如果Docker已经成功安装&#xff0c;你将看到Docker的版本信息。 下载mysql的镜像 Docker Hub是一个存储…...

前端DOM操作精解:基础概念、方法与最佳实践

引言 本文将深入探讨前端开发中的DOM操作&#xff0c;包括基础概念、常用方法和最佳实践。通过清晰易懂的解释和实际案例分析&#xff0c;我们将一起了解如何最有效地使用DOM操作来提升前端应用的用户体验。 一、DOM操作入门 在深入探讨DOM操作之前&#xff0c;我们先要理解…...

python sorted函数详解2023.9.11

sorted函数详解 1. 输入和输出2. key传入函数 1. 输入和输出 help(sorted) Help on built-in function sorted in module builtins: sorted(iterable, /, *, keyNone, reverseFalse)Return a new list containing all items from the iterable in ascending order.A custom k…...

Spring Reactive:响应式编程与WebFlux的深度探索

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…...

Qt应用开发(基础篇)——工具按钮类 QToolButton

一、前言 QToolButton类继承于QAbstractButton&#xff0c;该部件为命令或选项提供了一个快速访问按钮&#xff0c;通常用于QToolBar中。 按钮基类 QAbstractButton QToolButton是一个特殊的按钮&#xff0c;一般显示文本&#xff0c;只显示图标&#xff0c;结合toolBar使用。它…...

【数据结构面试题】栈与队列的相互实现

目录 1.队列实现栈 1.1创建栈 1.2判断是否为空 1.3入栈 1.4出栈 1.5获取栈顶元素 1.6完整代码 2. 用栈实现队列 2.1创建队列 2.2判断是否为空 2.3入队列 2.4出队列 2.5获取队头元素 2.6完整代码 1.队列实现栈 用队列实现栈https://leetcode.cn/problems/impleme…...

华为认证和红帽认证哪个比较好考呢

华为认证和红帽认证的考试难度、学习内容、适用范围等方面都有所不同&#xff0c;因此哪个比较好考要视具体情况而定&#xff1a; 考试难度&#xff1a;红帽认证的考试难度较高&#xff0c;需要考生具备较高的技术水平和实践经验&#xff1b;而华为认证则更注重基础知识的考察…...

[Java]_[中级]_[使用okhttp3和HttpClient代理访问外部网络]

场景 Java的http库常用的有HttpClient和Okhttp3, 如果公司有限制网络访问&#xff0c;需要代理才可以访问外网&#xff0c;那么如何使用代理Proxy&#xff1f; <dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient<…...

ubuntu 20.04 docker 安装 mysql

要在Ubuntu 20.04上安装Docker并运行MySQL容器&#xff0c;您可以按照以下步骤操作&#xff1a; 1.更新系统包列表&#xff1a; sudo apt update2.安装Docker&#xff1a; sudo apt install docker.io3.启动Docker服务并设置其开机自启动&#xff1a; sudo systemctl start…...

C++在C语言基础上的优化

目录 一、命名空间 1、命名空间的定义 2、命名空间的使用 二、输入&输出 三、缺省参数 1、缺省参数的概念 2、缺省参数的分类 四、函数重载 五、引用 1.引用的概念 2.引用的特性 3、引用和指针的区别 六、内联函数 七、基于范围的for循环 一、命名空间 命名空…...

分享一个python实验室设备预约管理系统 实验室设备维修系统源码 lw 调试

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…...

兵者多诡(HCTF2016)

环境:https://github.com/MartinxMax/CTFer_Zero_one 题目简介 解题过程 登录首页 提交png图片上传抓包&#xff0c;可以看到是向upload文件提交数据 在fp参数中尝试伪协议读取home.php文件 http://127.0.0.1:88/HCTF2016-LFI/home.php?fpphp://filter/readconvert.base64…...

【JAVA-Day04】Java关键字和示例:深入了解常用关键字的用法

Java关键字和示例&#xff1a;深入了解常用关键字的用法 摘要Java 关键字、标识符和命名规范一、Java 关键字常用关键字DEMO1. 示例代码使用 if 和 else 关键字&#xff1a;2. 示例代码使用 for 循环&#xff1a;3. 示例代码使用 switch 关键字&#xff1a;4. 示例代码使用 wh…...

Android请求网络报错:not permitted by network security policy

一、错误记录 https的接口请求正常的&#xff0c; 请求http的接口时报错&#xff1a;not permitted by network security policy 二、问题分析 原因&#xff1a; 由于 Android P(版本27以上) 限制了明文流量的网络请求&#xff0c;非加密的流量请求都会被系统禁止掉。如果当…...

python报错:ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1

python报错&#xff1a;ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1 问题分析 说明&#xff1a;requests包引入了urllib3&#xff0c;而新版本的urllib3 需要OpenSSL 1.1.1以上版本&#xff0c;否则报错&#xff1a; ImportError: urllib3 v2.0 only supports Ope…...

如何使用adb command来设置cpu频率和核数

透過ADB Shell設定CPU開核與freq的command與用法如下: # Disable PPM echo 0 > /proc/ppm/enabled # Enable PPM (Default) echo 1 > /proc/ppm/enabled echo 0 > /proc/ppm/enabled Fixed # Core for each cluster echo X Y > /proc/ppm/policy/ut_fix_core_num …...

236. 二叉树的最近公共祖先

236. 二叉树的最近公共祖先 题目-中等难度示例1. dfs 题目-中等难度 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p…...

Git常见问题:git pull 和 git pull --rebase二者区别

git pull 和 git pull --rebase 都是从远程仓库获取最新的更改并将其合并到本地分支。但它们之间的区别在于合并方式。以下是它们之间的主要区别&#xff1a; git pull&#xff1a; 当你执行 git pull 时&#xff0c;Git 会执行以下两个操作&#xff1a; git fetch&#xff…...

关于HarmonyOS元服务的主题演讲与合作签约

一、感言 坚持中&#xff0c;总会有很多意想不到的收获。 前几次参与HDC时更多的是观众、开发者、专家的身份&#xff0c;以参观、学习、交流为主。 通过几年的努力&#xff0c;和HarmonyOS功能成长&#xff0c;在2023年的HDC大会中&#xff0c;有了我的演讲&#xff0c;并带领…...

cache 学习

好文章&#xff1a; Cache的基本原理 - 知乎...

SSM - Springboot - MyBatis-Plus 全栈体系(六)

第二章 SpringFramework 四、SpringIoC 实践和应用 3. 基于 注解 方式管理 Bean 3.1 实验一&#xff1a;Bean 注解标记和扫描 (IoC) 3.1.1 注解理解 和 XML 配置文件一样&#xff0c;注解本身并不能执行&#xff0c;注解本身仅仅只是做一个标记&#xff0c;具体的功能是框…...

【Flutter】引入网络图片时,提示:Failed host lookup: ‘[图片host]‘

在使用 NetworkImage 组件加载外部图片时&#xff0c;提示 Failed host lookup: [图片host] 错误。 排查方向 1、清理缓存 解决方案&#xff1a; 尝试flutter clean清空缓存后重新安装依赖flutter pub get重新构建项目flutter create . 走完上述三个步骤后&#xff0c;再次…...

Python基础教程:索引和切片

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 索引&#xff08;下标&#xff09; 索引又称下标&#xff0c;用来表示可迭代对象中的某个元素的位置。 用正整数表示的索引值&#xff0c;从左向右定位&#xff0c;从 0 开始计数&#xff0c;如 0&#xff0c;1&#…...

JVM基础面试题

JDK、JRE、JVM的关系 JVM Java虚拟机&#xff0c;它只识别.class类型文件&#xff0c;它能将class文件中的字节码指令进行识别并调用操作系统向上的API完成动作。 JRE Java运行时环境。它主要包含两部分&#xff1a;Jvm的标准实现和Java的一些基本类库。相对于JVM来说,JRE多出来…...

网站建设3a模型是什么/天天seo伪原创工具

2016-04-16 回答故障之“automation 错误”之一 环境描述&#xff1a; 数据库服务器&#xff1a;database server(windows advanced server 2000) 中间层服务器&#xff1a;middleware server(windows advanced server 2000) 域服务器&#xff1a;domain server(windows advanc…...

一学一做短视频网站/在线种子资源网

【实例简介】网上找的一个例子&#xff0c;开始不能用&#xff0c;改后学习完&#xff0c;就共享出来大家学习【实例截图】【核心代码】25199250-84c1-44bc-998d-45eba6b3b78b└── SWFTest├── src│ └── org│ └── demo│ └── tomcat│ └── UploadSer…...

wordpress文本自动分页/上海百度推广

Cuda安装 安装cuda是比较麻烦的一步&#xff0c;以下安装说明来自cuda的安装说明文件。遇到问题找官方解决方案&#xff0c;最便捷有效。Perform the following steps to install CUDA and verify the installation.Disable the Nouveau drivers:首先需要屏蔽ubuntu默认的显卡驱…...

文化传播公司网站建设需求/属于seo网站优化

最近在学分治法&#xff0c;涉及到很多问题都能用分治法解决&#xff0c;汉诺塔&#xff0c;快排&#xff0c;归并排序等&#xff0c;下面是解决最大字串问题。 1&#xff0c;用分治法解决&#xff0c;下面还介绍不用分治解决的另一种方法。 #include<stdio.h> //分治法…...

厚街h5网站建设/今日国内新闻头条15条

题目&#xff1a; 给定一个链表&#xff0c;每个节点包含一个额外增加的随机指针&#xff0c;该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的深拷贝。 示例&#xff1a; 输入&#xff1a; {"$id":"1","next":{"$id"…...

旅游景点网站建设设计说明/江东怎样优化seo

暖气来了&#xff0c;嗓子眼儿冒火、口腔溃疡、大便干燥&#xff0c;该怎么办呢&#xff1f;解放军309医院营养科主任医师张晔开出四字饮食处方&#xff1a;降、清、润、补。 降火汤——冬瓜配紫菜 很多家庭最爱做西红柿黄瓜片汤&#xff0c;其实冬季最好的汤是冬瓜汤&#xff…...