近况

好久没写博客了,其实想写的有很多,但是最近都在看软考的课程,软件设计师的考题范围太广了,
网课内容太多了,我都没把握把软考的网课看完,每天上晚班累死,到家确实实际学习时间没有多少了
害,这软考能不能过还得看运气了,先尽力准备软考吧。

废话不多说,水一篇关于后端处理数据库中具有层次结构的数据并将其转成树形结构数据的博客

开发场景

在开发过程中,经常有一些具有层次结构的功能展示,比如菜单下有子菜单,部门下有子部门,省市县.
这些具有层次结构的数据在数据库表中是保存在同一张表中的,每条数据通过一个parentId字段用于区分数据上下级关系
下面是一张菜单表,其中可以看到每一条数据的parentId指向了该条数据的上级数据,即父菜单,我们也可以通过
某一条数据的id查询出它的子菜单。

当然,开发中也有其他方式存储上下级层次数据,并不一定要通过parentId字段,但使用parentId是最常用的方式

树形结构数据的认识

数据库中带有层次结构的数据存在同一张表中,我们查出来是一个List集合,但是前端树组件需要是树形结构数据
所以需要将查询出来的数据库List数据,根据上下级关系,根据它们的层次结构,构造成树形结构数据,
这个转换工作前后端都可以进行实现,本篇博客主要讨论后端是如何将原始List数据转成树形结构数据的。
下面是树形结构数据的样例展示。

[
  {
    "id": "1",
    "parentId": "0",
    "weight": 5,
    "name": "系统管理",
    "extraField": 666,
    "other": "java.lang.Object@27ddd392",
    "children": [
      {
        "id": "11",
        "parentId": "1",
        "weight": 222222,
        "name": "用户管理",
        "extraField": 666,
        "other": "java.lang.Object@19e1023e",
        "children": [
          {
            "id": "111",
            "parentId": "11",
            "weight": 0,
            "name": "用户添加",
            "extraField": 666,
            "other": "java.lang.Object@7cef4e59"
          }
        ]
      }
    ]
  },
  {
    "id": "2",
    "parentId": "0",
    "weight": 1,
    "name": "店铺管理",
    "extraField": 666,
    "other": "java.lang.Object@4c3e4790",
    "children": [
      {
        "id": "21",
        "parentId": "2",
        "weight": 1,
        "name": "商品管理1",
        "extraField": 666,
        "other": "java.lang.Object@38cccef"
      },
      {
        "id": "22",
        "parentId": "2",
        "weight": 2,
        "name": "商品管理2",
        "extraField": 666,
        "other": "java.lang.Object@5679c6c6"
      }
    ]
  }
]

这个树形结构数据就是前端树组件所需要的格式,最外一层的数组中有两个元素,这个两个元素属于同一级别
系统管理,店铺管理菜单属于一级菜单,而元素的children属性则关联着他们的子菜单。当然他们的子元素都
可以有children,都可以有它们的子菜单,而上面这个例子中最里层的元素并没有子元素,是因为我们可以控制
转换的深度,即可以指定只需要前3层的数据,不展示3层以下的数据,这是可以实现的。

转换数据的两种方式

将从数据库查询出来的List数据转成树形结构数据有多种方式,这里介绍两种最常见的两种方式

  • 通过递归的方式,手动写递归代码来组装数据,使得形成树形结构数据

    • 创建具有树形结构的Vo类
      我们从数据库中查询出来的对象一般是表对应的实体类,但是页面中返回的数据中需要children字段的
      所以我们需要先创建一个Vo类,将实体类中所有字段全部拷贝一分到Vo类型,
      再在Vo类中添加一个List<Vo类>属性,属性名为children,用于存储子元素,因为子元素也有children
      所以List的泛型不是实体类,而是Vo类。

    • 分别查询出一级菜单和非一级菜单(子菜单),并分别将实体类集合封装为Vo集合(List<Vo类>)
      这里的一级菜单指的是顶级菜单,在最上面的数据库菜单表图片中,就是parentId为0的几条数据
      而不是id为0的数据,非一级菜单就是一级菜单以下级别的菜单数据,全部一次性查出来
      这里有一个注意点,一次性查询出所有数据,再对数据进行组装,比根据菜单id去数据库查询其子菜单数据要快的多。另外有一个注意的地方是,将实体类数据封装成Vo对象,可以使用BeanUtil的浅拷贝方式

    • 递归组装数据
      完成第二步后,我们有了一级菜单数据的Vo集合和二级菜单的Vo集合数据,集合中是一个个Vo对象
      不过这些对象中的children属性都没有数据,现在我们就需要通过递归来实现数据的组装,
      下面是一个例子:

      //代码中调用递归方法
      getChildren(treeList,recordList);
      
      private void getChildren(List<MenuVo> treeList,List<MenuVo> recordList) {
          //treeList是一级菜单集合,recordList是子菜单集合包括二级以下菜单
          treeList.forEach(menuVo -> {
              recordList.forEach(record -> {
                  if (ObjectUtil.equals(record.getParentId(),menuVo.getId())){
                      menuVo.getChildren().add(record);
                  }
              });
              getGrandChildren(menuVo.getChildren(), recordList);
          });
      }

      执行完递归方法后,则我们的一级菜单Vo集合就有children数据了,并且children中的元素也有children数据了
      最后将一级菜单Vo集合返回给前端即可。

    • 2023-7-14更新,同样使用递归方式查找出某个节点的子节点接口

      /**
      * 列表数据
      */
      @AutoLog(value = "数据文件检测配置-列表数据")
      @ApiOperation(value="数据文件检测配置-列表数据", notes="数据文件检测配置-列表数据")
      @GetMapping(value = "/getById")
      public Result<?> queryPageList(@RequestParam("id")String id) {
        //检查
        if (ObjectUtil.isEmpty(id)){
          return Result.error("id不可为空");
        }
      
        List<EamsDatafileVerifyConfig> list = eamsDatafileVerifyConfigService.list();
        EamsDatafileVerifyConfig config = new EamsDatafileVerifyConfig();
        for (EamsDatafileVerifyConfig eamsDatafileVerifyConfig : list) {
          if (ObjectUtil.equals(eamsDatafileVerifyConfig.getId(),id)){
            config=eamsDatafileVerifyConfig;
            break;
          }
        }
        if (ObjectUtil.isEmpty(config)){
          return Result.error("数据库中没有该数据");
        }
      
        //查指定节点下的数据
        getSubNodes(list,config);
        return Result.OK(config);
      }
      
      /**
      	 * 根据id值,获取数据及其子数据
      	 * @param configs
      	 * @param verifyConfig
      	 */
      public void getSubNodes(List<EamsDatafileVerifyConfig> configs,EamsDatafileVerifyConfig verifyConfig){
        for (EamsDatafileVerifyConfig config : configs) {
          if (ObjectUtil.equals(config.getParentId(),verifyConfig.getId())){
            ArrayList<EamsDatafileVerifyConfig> children = verifyConfig.getChildren();
            if (ObjectUtil.isEmpty(children)){
              ArrayList<EamsDatafileVerifyConfig> childs = new ArrayList<>();
              childs.add(config);
              verifyConfig.setChildren(childs);
            }else{
              children.add(config);
              verifyConfig.setChildren(children);
            }
            getSubNodes(configs,config);
          }
        }
      }
  • 通过使用hutool中的TreeUtil工具类来构造树形结构数据
    使用递归的方式来构建树形结构数据有几点不好
    第一是我们需要去创建一个Vo类,拷贝所有实体类数据到Vo类中。
    第二是如果前端树组件需要的元素中并不需要属性名为id,而是uid或者其他属性名,
    又或者树组件还需要其他属性,比如title,比如value属性,那我们不得不在Vo类中新增这些属性,
    并在将实体类集合拷贝到Vo类的时候,为这些新增属性赋值。
    第三是递归可能编写程序复杂,并不适合像我这种逻辑性差的人

    综上所述,使用hutool中的TreeUtil工具类可能更方便,下面是一段使用TreeUtil工具类实现构建树形结构数据的代码

    //模拟的从数据库查询出来的数据
    List<FileCategoryDO> fileCategoryDOS = new ArrayList<>();
    // 构建node列表
    List<TreeNode<String>> nodeList = CollUtil.newArrayList();
    
    nodeList = fileCategoryDOS.stream().map(fileCategoryDO -> {
        Map<String, Object> map = new HashMap<>();
        /*
            前端树组件有时候并不只需要id,parentId,name树形,可能还需要其他属性
            也就是说,我们想输出的时候每个元素还可以输出实体类的其他属性,这个时候就可以为TreeNode配置一个map
            在map中key为新增属性,value则是元素对应的值
        */
        map.put("level", "2");
        map.put("gmt_create", LocalDateTime.now());
        map.put("gmt_modifier", LocalDateTime.now());
        /*
            这里的TreeNode泛型为String,则我们向结点保存时,数据需要为String类型的
            如果即想要String类型又想要Integer类型,泛型可以设置为Object类型
        */
        TreeNode<String> treeNode = new TreeNode<String>().setId(fileCategoryDO.getId())
            .setName(fileCategoryDO.getCategoryName())
            .setParentId(fileCategoryDO.getPid())
            .setWeight(fileCategoryDO.getSort())
            .setExtra(map);
        return treeNode;
    }).collect(Collectors.toList());
    
    //结点配置
    TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
    /*
        自定义属性名,当前端树组件需要的属性名并不是id,children时,我们可以设置为其他属性名称
        这样配置好后,id,children属性名将改成这些属性名
    */ 
    treeNodeConfig.setWeightKey("orderNum");
    treeNodeConfig.setIdKey("id");
    treeNodeConfig.setChildrenKey("childrenNode");
    // 最大递归深度
    treeNodeConfig.setDeep(3);
     
    //转换器
    List<Tree<String>> treeNodes = TreeUtil.build(nodeList, "0", treeNodeConfig,
      (treeNode, tree) -> {
        /*
            这里就是配置输出的数据来自TreeNode数据的哪些字段,
            并且tree还要配置新增的属性,之前是将新增的属性保存在TreeNode结点中
            现在是将这些值取出来并配置在tree中。
        */
        tree.setId(treeNode.getId());
        tree.setParentId(treeNode.getParentId());
        tree.setWeight(treeNode.getWeight());
        tree.setName(treeNode.getName());
        // 扩展属性 ...
        tree.putExtra("level", treeNode.getExtra().getOrDefault("level", 2));
        tree.putExtra("gmt_create", treeNode.getExtra().getOrDefault("gmt_create", null));
    });
    
    //拿到最终树形结构数据,还要转换成json格式数据
    System.out.println(JSONUtil.toJsonStr(treeNodes).toString());

2022年9月26日补充

当我们想要删除具有树形结构的数据时,通常也需要删除这条被删除数据的所有下级数据
而如果我们每次找到一级数据就立马删除的话,删除效率并不是太高,因为需要多次连接数据库进行删除
正确方式是:通过递归找到所有下级数据的id,将其全部保存到一个ArrayList中,然后再批量删除,
这样我们只需要与数据库建立一次链接即可。

//id为需要被删除的数据,idList则是保存所有将被删除数据的id,以便之后批量一次性删除
private void checkChildrenExists(String id, List<String> idList) {
  LambdaQueryWrapper<SysDepart> query = new LambdaQueryWrapper<>();
  query.eq(SysDepart::getParentId,id);
  List<SysDepart> departList = this.list(query);
  if(departList != null && departList.size() > 0) {
    for(SysDepart depart : departList) {
      idList.add(depart.getId());
      this.checkChildrenExists(depart.getId(), idList);
    }
  }
}

推荐博客