【实战案例】树形字典结构数据的后端解决方案

在管理系统中,经常会有树形结构的数据需求,例如如图所示的课程一级分类和二级分类

对应这样的情况在数据库层面就需要对字段进行设计,表字段信息和示例数据说明如下图所示

通过上述说明可以看出这张表的数据是树形结构,通过根节点为0的ID可以检索到表中所有数据(前提是一棵完整的树),所以在实际项目中,后端需要一个接口,用于给前端提供该课程分类表的数据,且是树形结构的形态返回给前端。

下面进入实际代码:

课程分类的PO类(PO类是与数据库直接一一对应的)

java 复制代码
package com.gavin.content.model.po;

import java.io.Serializable;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 课程分类
 * @author gavin
 */
@Data
@TableName("course_category")
public class CourseCategory implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private String id;

    /**
     * 分类名称
     */
    private String name;

    /**
     * 分类标签默认和名称一样
     */
    private String label;

    /**
     * 父结点id(第一级的父节点是0,自关联字段id)
     */
    private String parentid;

    /**
     * 是否显示
     */
    private Integer isShow;

    /**
     * 排序字段
     */
    private Integer orderby;

    /**
     * 是否叶子
     */
    private Integer isLeaf;
}

定义DTO类继承PO表示分类信息的模型(DTO通常用于数据转化)

java 复制代码
package com.gavin.content.model.dto;

import com.gavin.content.model.po.CourseCategory;
import lombok.Data;

import java.io.Serializable;
import java.util.List;
/**
 * @author Gavin
 * @description 课程分类信息模型类
 * @date 2024/10/14
 **/
@Data
public class CourseCategoryTreeDto extends CourseCategory implements Serializable {

  List<CourseCategoryTreeDto> childrenTreeNodes;
}

数据字典控制器

java 复制代码
package com.gavin.content.api;

import com.gavin.content.model.dto.CourseCategoryTreeDto;
import com.gavin.content.service.CourseCategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 课程分类 前端控制器
 * @author gavin
 */
@Slf4j
@RestController
public class CourseCategoryController {

    @Autowired
    private CourseCategoryService  courseCategoryService;

    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes() {
        return courseCategoryService.queryTreeNodes("1");
    }
}

接下来的接口实现的逻辑是核心。因为这里表的层级结构是固定的,总共就两级,所以可以直接用拼接的方式定义SQL语句,如下

sql 复制代码
select
       one.id            one_id,
       one.name          one_name,
       one.parentid      one_parentid,
       one.orderby       one_orderby,
       one.label         one_label,
       two.id            two_id,
       two.name          two_name,
       two.parentid      two_parentid,
       two.orderby       two_orderby,
       two.label         two_label
   from course_category one
            inner join course_category two on one.id = two.parentid
   where one.parentid = 1
     and one.is_show = 1
     and two.is_show = 1
   order by one.orderby,
            two.orderby

上述过程使用INNER内连接将同一个表course_category连接两次,条件是子分类的parentid与父分类的id匹配。所以这条SQL查询的作用就是,从course_category表中查找所有parentid为1的父分类,并显示其下属的子分类(同时满足is_show为1),返回的结果按照父分类和子分类的排序顺序排列。

但是上述过程还是有局限性的,因为层级固定,通常情况下可以采用递归遍历的方式来实现查询,使用WITH RECURSIVE语法进行实现,示例如下:

sql 复制代码
WITH RECURSIVE t1 AS
(
  SELECT 1 AS n              -- 初始查询:设定递归查询的起始值为 1
  UNION ALL
  SELECT n + 1 FROM t1       -- 递归部分:每次递归查询在前一次的基础上加 1
  WHERE n < 5                -- 递归条件:当 n >= 5 时终止递归
)
SELECT * FROM t1;            -- 查询递归生成的结果集

上述过程就展现了生成1到5的数字序列。

根据上述的逻辑对应到上述的课程分类表中,SQL语句如下:

sql 复制代码
WITH RECURSIVE t1 AS 
(
    SELECT * FROM course_category p WHERE id = '1'
    UNION ALL
    SELECT t.* FROM course_category t INNER JOIN t1 ON t1.id = t.parentid
)
SELECT * FROM t1 ORDER BY t1.id, t1.orderby;

上述过程是从上往下递归,即根节点为"1",如果要实现从下往上递归则修改初始节点及条件即可,如下:

sql 复制代码
WITH RECURSIVE t1 AS (
    SELECT * FROM course_category p WHERE id = '1-1-1'
    UNION ALL
    SELECT t.* FROM course_category t INNER JOIN t1 ON t1.parentid = t.id
)
SELECT * FROM t1 ORDER BY t1.id, t1.orderby;

mysql为了避免无限递归默认递归次数为1000,可以通过设置cte_max_recursion_depth参数增加递归深度,还可以通过max_execution_time限制执行时间,超过此时间也会终止递归操作。

mysql递归相当于在存储过程中执行若干次sql语句,java程序仅与数据库建立一次链接执行递归操作,所以只要控制好递归深度,控制好数据量性能就没有问题。

思考:如果java程序在递归操作中连接数据库去查询数据组装数据,这个性能高吗?

参考回答:不会太高,因为多次数据库连接会有开销,查询次数多且事务处理复杂会导致响应时间过长。所以除了上述递归查询,还可以使用批量查询或缓存进内存的方法提高性能(因为字典数据通常情况下变化不大)

回到主线,定义mapper接口及查询语句

java 复制代码
package com.gavin.content.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gavin.content.model.dto.CourseCategoryTreeDto;
import com.gavin.content.model.po.CourseCategory;

import java.util.List;

/**
 * 课程分类 Mapper 接口
 * @author gavin
 */
public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    public List<CourseCategoryTreeDto> selectTreeNodes(String id);
}
java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gavin.content.mapper.CourseCategoryMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.gavin.content.model.po.CourseCategory">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="label" property="label" />
        <result column="parentid" property="parentid" />
        <result column="is_show" property="isShow" />
        <result column="orderby" property="orderby" />
        <result column="is_leaf" property="isLeaf" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, label, parentid, is_show, orderby, is_leaf
    </sql>

    <select id="selectTreeNodes" resultType="com.gavin.content.model.dto.CourseCategoryTreeDto" parameterType="string">
        with recursive t1 as (
            select * from  course_category p where  id= #{id}
            union all
            select t.* from course_category t inner join t1 on t1.id = t.parentid
        )
        select *  from t1 order by t1.id, t1.orderby
    </select>
</mapper>

定义service接口及实现类

java 复制代码
package com.gavin.content.service;

import com.gavin.content.model.dto.CourseCategoryTreeDto;

import java.util.List;

public interface CourseCategoryService {
    /**
     * 课程分类树形结构查询
     */
    public List<CourseCategoryTreeDto> queryTreeNodes(String id);
}
java 复制代码
package com.gavin.content.service.impl;

import com.gavin.content.mapper.CourseCategoryMapper;
import com.gavin.content.model.dto.CourseCategoryTreeDto;
import com.gavin.content.service.CourseCategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {

    @Autowired
    CourseCategoryMapper courseCategoryMapper;

    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
       List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
    //通过Java Stream API,将courseCategoryTreeDtos转换为一个Map<String, CourseCategoryTreeDto>
    //过滤掉根节点,即id.equals(item.getId())为true的元素不参与转换
    //collect(Collectors.toMap(...)) 用于将列表转换为Map
    //key -> key.getId():节点的ID作为Map的键
	//value -> value:节点对象作为Map的值
	//(key1, key2) -> key2:处理键冲突的情况,如果有重复键,则保留最新的值。
    Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream()
    .filter(item -> !id.equals(item.getId()))
    .collect(Collectors.toMap(
        key -> key.getId(), 
        value -> value, 
        (key1, key2) -> key2
    ));

    //最终返回的list
    List<CourseCategoryTreeDto> categoryTreeDtos = new ArrayList<>();
    //依次遍历每个元素,filter(item -> !id.equals(item.getId()))过滤掉根节点
    //对每个节点进行处理:
    //   添加到顶级节点列表:如果当前节点的parentid等于根节点id,则将其添加到categoryTreeDtos列表
    //   构建子节点:
    //     从mapTemp中找到当前节点的父节点对象(courseCategoryTreeDto) 
    //     如果找到的父节点不为空,且它的childrenTreeNodes属性为空,则初始化它为一个空的List
    //     将当前节点作为子节点添加到父节点的childrenTreeNodes列表中
    courseCategoryTreeDtos.stream()
    .filter(item -> !id.equals(item.getId()))
    .forEach(item -> {
        if (item.getParentid().equals(id)) {
            categoryTreeDtos.add(item);
        }
        CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());
        if (courseCategoryTreeDto != null) {
            if (courseCategoryTreeDto.getChildrenTreeNodes() == null) {
                courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
            }
            courseCategoryTreeDto.getChildrenTreeNodes().add(item);
        }
    });

    return categoryTreeDtos;
    }
}

定义单元测试类对service接口进行测试

java 复制代码
package com.gavin.content;

import com.gavin.content.model.dto.CourseCategoryTreeDto;
import com.gavin.content.service.CourseCategoryService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class CourseCategoryServiceTests {

    @Autowired
    CourseCategoryService courseCategoryService;

    @Test
    void testqueryTreeNodes() {
        List<CourseCategoryTreeDto> categoryTreeDtos = courseCategoryService.queryTreeNodes("1");
        System.out.println(categoryTreeDtos);
    }
}

测试后返回的数据结构示例如下所示:

javascript 复制代码
[
  {
    "id": "1-1",
    "name": "前端开发",
    "label": "前端开发",
    "parentid": "1",
    "isShow": 1,
    "orderby": 1,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-1-1",
        "name": "HTML/CSS",
        "label": "HTML/CSS",
        "parentid": "1-1",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-1-2",
        "name": "JavaScript",
        "label": "JavaScript",
        "parentid": "1-1",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-1-3",
        "name": "Vue",
        "label": "Vue",
        "parentid": "1-1",
        "isShow": 1,
        "orderby": 9,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-2",
    "name": "移动开发",
    "label": "移动开发",
    "parentid": "1",
    "isShow": 1,
    "orderby": 2,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-2-1",
        "name": "微信开发",
        "label": "微信开发",
        "parentid": "1-2",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-2-2",
        "name": "iOS",
        "label": "iOS",
        "parentid": "1-2",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-3",
    "name": "编程开发",
    "label": "编程开发",
    "parentid": "1",
    "isShow": 1,
    "orderby": 3,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-3-1",
        "name": "C/C++",
        "label": "C/C++",
        "parentid": "1-3",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-3-2",
        "name": "Java",
        "label": "Java",
        "parentid": "1-3",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-4",
    "name": "数据库",
    "label": "数据库",
    "parentid": "1",
    "isShow": 1,
    "orderby": 4,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-4-1",
        "name": "Oracle",
        "label": "Oracle",
        "parentid": "1-4",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-4-2",
        "name": "MySQL",
        "label": "MySQL",
        "parentid": "1-4",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-5",
    "name": "人工智能",
    "label": "人工智能",
    "parentid": "1",
    "isShow": 1,
    "orderby": 5,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-5-1",
        "name": "机器学习",
        "label": "机器学习",
        "parentid": "1-5",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-5-2",
        "name": "深度学习",
        "label": "深度学习",
        "parentid": "1-5",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-6",
    "name": "云计算/大数据",
    "label": "云计算/大数据",
    "parentid": "1",
    "isShow": 1,
    "orderby": 6,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-6-1",
        "name": "Spark",
        "label": "Spark",
        "parentid": "1-6",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-6-2",
        "name": "Hadoop",
        "label": "Hadoop",
        "parentid": "1-6",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-7",
    "name": "UI设计",
    "label": "UI设计",
    "parentid": "1",
    "isShow": 1,
    "orderby": 7,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-7-1",
        "name": "Photoshop",
        "label": "Photoshop",
        "parentid": "1-7",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-8",
    "name": "游戏开发",
    "label": "游戏开发",
    "parentid": "1",
    "isShow": 1,
    "orderby": 8,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-8-1",
        "name": "Cocos",
        "label": "Cocos",
        "parentid": "1-8",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-8-2",
        "name": "Unity3D",
        "label": "Unity3D",
        "parentid": "1-8",
        "isShow": 1,
        "orderby": 2,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  },
  {
    "id": "1-9",
    "name": "智能硬件/物联网",
    "label": "智能硬件/物联网",
    "parentid": "1",
    "isShow": 1,
    "orderby": 9,
    "isLeaf": 0,
    "childrenTreeNodes": [
      {
        "id": "1-9-1",
        "name": "无线通信",
        "label": "无线通信",
        "parentid": "1-9",
        "isShow": 1,
        "orderby": 1,
        "isLeaf": 1,
        "childrenTreeNodes": null
      },
      {
        "id": "1-9-10",
        "name": "物联网技术",
        "label": "物联网技术",
        "parentid": "1-9",
        "isShow": 1,
        "orderby": 10,
        "isLeaf": 1,
        "childrenTreeNodes": null
      }
    ]
  }
]

至此,树形结构的字段数据的后端解决方案完成,对于类似结构或业务可推而广之。

相关推荐
劲夫学编程44 分钟前
leetcode:杨辉三角
算法·leetcode·职场和发展
毕竟秋山澪1 小时前
孤岛的总面积(Dfs C#
算法·深度优先
阿伟*rui2 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
待磨的钝刨2 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
浮生如梦_3 小时前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
XiaoLeisj4 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck4 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei4 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师5 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉5 小时前
创建线程时传递参数给线程
开发语言·c++·算法