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

news2024/12/26 10:51:38

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转换为DeptNode
    List<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 {

    @Resource
    private 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.结语

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/988846.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

激光切割机在船舶行业的的应用有哪些

我国享有世界工厂的美誉&#xff0c;是全球制造业的主力。然而&#xff0c;在船舶制造的关键技术领域&#xff0c;我国的研发投入不足&#xff0c;技术进步仍滞后&#xff0c;我国高端船舶制造的实力仍显不足。 在我国制造业全面复苏的当前背景下&#xff0c;“精准制作”正构成…

苹果上架app备案流程介绍

摘要&#xff1a;本文将为iOS技术博主介绍苹果上架App备案流程的详细步骤&#xff0c;包括注册开发者账号、创建App ID、创建证书、创建Provisioning Profile、开发应用程序、提交应用程序、审核和上架。了解这一流程对于想要将应用程序上架到App Store供用户下载使用的博主来说…

靶场溯源第二题

关卡描述&#xff1a;1. 网站后台登陆地址是多少&#xff1f;&#xff08;相对路径&#xff09; 首先这种确定的网站访问的都是http或者https协议&#xff0c;搜索http看看。关于http的就这两个信息&#xff0c;然后172.16.60.199出现最多&#xff0c;先过滤这个ip看看 这个很…

FAT32文件系统f_mkfs函数详解

1.f_mkfs参数 参数path&#xff1a;要挂载/卸载的逻辑驱动器号;使用设备根路径表示。 参数opt&#xff1a;系统的格式&#xff0c;如图所示&#xff0c;选择FM_FAT32即可&#xff0c;选择其他的可能无法格式化。 参数au&#xff1a;每簇的字节数&#xff0c;以字节为单位&#…

无涯教程-JavaScript - IMDIV函数

描述 IMDIV函数以x yi或x yj文本格式返回两个复数的商。 $$IMDIV(z1,z2) \frac {(a bi)} {(c in)} \frac {{ac bd)(bc-ad)i} {c ^ 2 d ^ 2 } $$ 语法 IMDIV (inumber1, inumber2)争论 Argument描述Required/OptionalInumber1The complex numerator or dividend.Req…

异步编程 - 11 Spring WebFlux的异步非阻塞处理

文章目录 概述Spring WebFlux概述Reactive编程&Reactor库WebFlux服务器WebFlux的并发模型WebFlux对性能的影响WebFlux的编程模型WebFlux注解式编程模型WebFlux函数式编程模型 WebFlux原理浅尝Reactor Netty概述WebFlux服务器启动流程WebFlux一次服务调用流程 WebFlux的适用…

使用gradle打包springboot项目

这边整理下自己项目使用gradle打jar包的坎坷经历&#xff0c;使用的方式是命令行的方式 首先配置build.gradle跟我一样 plugins {id javaid org.springframework.boot version 3.1.3id io.spring.dependency-management version 1.1.3 }//用于添加Java插件,以及一些内置任务&a…

springboot+redis

1.pom.xml <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> 2.yml配置 # redis 配置redis:host: 127.0.0.1#超时连接timeout: 1000msjedis:pool:#最大连…

OpenHarmony:如何使用HDF驱动控制LED灯

一、程序简介 该程序是基于OpenHarmony标准系统编写的基础外设类&#xff1a;RGB LED。 目前已在凌蒙派-RK3568开发板跑通。详细资料请参考官网&#xff1a;https://gitee.com/Lockzhiner-Electronics/lockzhiner-rk3568-openharmony/tree/master/samples/b02_hdf_rgb_led。 …

十分钟,了解Kafka的Sender线程

〇、前言 在上两篇文章《连Producer端的主线程模块运行原理都不清楚&#xff0c;就敢说自己精通Kafka》和《一文了解Kafka的消息收集器RecordAccumulate》中&#xff0c;我们介绍了Main Thread和RecordAccumulate的工作原理&#xff0c;那么在本篇文章中&#xff0c;我们继续介…

Python之Xlwings操作excel

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、xlwings简介二、安装与使用1.安装2.使用3.xlwings结构说明 二、xlwings对App常见的操作App基础操作工作簿的基础操作工作表的基础操作工作表其他操作 读取单元格…

Android 自定义View之圆形进度条

很多场景下都用到这种进度条&#xff0c;有的还带动画效果&#xff0c; 今天我也来写一个。 写之前先拆解下它的组成&#xff1a; 底层圆形上层弧形中间文字 那我们要做的就是&#xff1a; 绘制底层圆形&#xff1b;在同位置绘制上层弧形&#xff0c;但颜色不同&#xff…

【0908练习】shell脚本使用expr截取网址

题目&#xff1a; 终端输入网址&#xff0c;如&#xff1a;www.hqyj.com&#xff0c; 要求&#xff1a;截取网址每个部分&#xff0c;并放入数组中&#xff0c;不能使用cut&#xff0c;使用expr解决 #!/bin/bash read -p "请输入一个网址" net lenexpr length $net …

协程 VS 线程,Kotlin技术精讲

协程(coroutines)是一种并发设计模式&#xff0c;您可以在Android 平台上使用它来简化异步执行的代码。协程是在版本 1.3 中添加到 Kotlin 的&#xff0c;它基于来自其他语言的既定概念。 在 Android 上&#xff0c;协程有助于管理长时间运行的任务&#xff0c;如果管理不当&a…

无脑014——linux系统,制作coco(json)格式数据集,使用mmdetection训练自己的数据集

电脑&#xff0c;linux&#xff0c;RTX 3090 cuda 11.2 1.制作coco&#xff08;json&#xff09;格式数据集 这里我们使用的标注软件是&#xff1a;labelimg 选择voc格式进行标注&#xff0c;标注之后使用以下代码&#xff0c;把voc格式转换成coco格式&#xff0c;注意最后的路…

机房运维管理软件不知道用哪个好?

云顷网络还原系统V7.0是一款专业的机房运维管理产品&#xff0c;基于局域网络环境&#xff0c;针对中高端机房中电脑运维管理需求所设计开发的。网络还原系统软件通过全面的规划和设计&#xff0c;遵从机房部署、使用到维护阶段化使用方式&#xff0c;通过极速网络同传/增量对拷…

TypeScript的函数

ts与js函数区别 tsjs传参需要规定类型无类型箭头函数箭头函数ES6函数类型无函数类型必填和可选参数所有参数都是可选的能设置默认参数能设置默认参数剩余参数剩余参数 函数重载 函数重载 注释 TypeScript 允许您指定函数的输入和输出值的类型。 输入值注释 // 传参必须为字…

如何理解图神经网络的傅里叶变换和图卷积

图神经网络&#xff08;GNN&#xff09;代表了一类强大的深度神经网络架构。在一个日益互联的世界里&#xff0c;因为信息的联通性&#xff0c;大部分的信息可以被建模为图。例如&#xff0c;化合物中的原子是节点&#xff0c;它们之间的键是边。图神经网络的美妙之处在于它们能…

【设计模式】二、UML 类图概述

文章目录 常见含义含义依赖关系&#xff08;Dependence&#xff09;泛化关系&#xff08;Generalization&#xff09;实现关系&#xff08;Implementation&#xff09;关联关系&#xff08;Association&#xff09;聚合关系&#xff08;Aggregation&#xff09;组合关系&#x…

【赠书活动】AI 时代,程序员无需焦虑

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…