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

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

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

通过上述说明可以看出这张表的数据是树形结构,通过根节点为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
      }
    ]
  }
]

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

相关推荐
Dream_Snowar5 分钟前
速通Python 第四节——函数
开发语言·python·算法
星河梦瑾7 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富11 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想13 分钟前
JMeter 使用详解
java·jmeter
言、雲15 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
1nullptr17 分钟前
三次翻转实现数组元素的旋转
数据结构
Altair澳汰尔19 分钟前
数据分析和AI丨知识图谱,AI革命中数据集成和模型构建的关键推动者
人工智能·算法·机器学习·数据分析·知识图谱
TT哇22 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A43 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
Python机器学习AI1 小时前
分类模型的预测概率解读:3D概率分布可视化的直观呈现
算法·机器学习·分类