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

server/2024/10/23 3:08:26/

在管理系统中,经常会有树形结构的数据需求,例如如图所示的课程一级分类和二级分类
在这里插入图片描述
对应这样的情况在数据库层面就需要对字段进行设计,表字段信息和示例数据说明如下图所示
在这里插入图片描述
在这里插入图片描述
通过上述说明可以看出这张表的数据是树形结构,通过根节点为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 {@Autowiredprivate CourseCategoryService  courseCategoryService;@GetMapping("/course-category/tree-nodes")public List<CourseCategoryTreeDto> queryTreeNodes() {return courseCategoryService.queryTreeNodes("1");}
}

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

selectone.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_labelfrom course_category oneinner join course_category two on one.id = two.parentidwhere one.parentid = 1and one.is_show = 1and two.is_show = 1order by one.orderby,two.orderby

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

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

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

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

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

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

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

WITH RECURSIVE t1 AS (SELECT * FROM course_category p WHERE id = '1-1-1'UNION ALLSELECT 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 allselect 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 {@AutowiredCourseCategoryMapper 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));//最终返回的listList<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 {@AutowiredCourseCategoryService courseCategoryService;@Testvoid 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}]}
]

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


http://www.ppmy.cn/server/134056.html

相关文章

ShardingProxy服务端分库分表

目录 一、为什么要有服务端分库分表&#xff1f; 二、ShardingProxy基础使用 1、部署ShardingProxy 2、配置常用分库分表策略 三、ShardingSphere中的分布式事务机制 1、什么是XA事务&#xff1f; 2、实战理解XA事务 3、如何在ShardingProxy中使用另外两种事务管理器&a…

数据库模式概述

数据库模式概述 在数据库系统中&#xff0c;数据库模式是数据库结构的描述&#xff0c;它定义了数据库中的数据如何组织、存储和管理。根据不同的层次&#xff0c;数据库模式可以分为三种类型&#xff1a;外模式&#xff08;External Schema&#xff09;、概念模式&#xff08…

什么是DDoS脉冲攻击?怎么防御?

DDoS攻击是一种旨在通过向目标服务器发送大量流量来使其服务中断的网络攻击。其中一种形式被称为“脉冲攻击”或“脉冲式DDoS攻击”&#xff0c;其特点是攻击流量以短暂而强烈的脉冲形式出现&#xff0c;而非持续不断的流量洪流。这种攻击方式的设计目的是为了躲避传统的DDoS检…

Molmo模型实战

安装pip文件 conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=11.8 -c pytorch -c nvidiapip install 

ASP.NET Core8.0学习笔记(二十一)——EFCore关系配置API

一、关系配置API概述 当我们需要指定一个字段作为外键&#xff0c;而这个外键又不符合以上四种约定时&#xff0c;就需要在IEntityTypeConfiguration实现类&#xff08;对应的配置类&#xff09;中使用Fluent API直接配置外键。理论上可以通过API直接指定一个属性&#xff0c;…

nginx反向代理下的长连接

一、nginx使用场景 大型应用架构中&#xff0c;一般会使用nginx反向代理&#xff0c;分为三层&#xff1a; 1.调用层&#xff0c;浏览器或APP&#xff1b; 2.中间层&#xff0c;反向代理nginx&#xff1b; 3.服务层&#xff0c;server一般是apche、tomcat 请求调用过程&…

算法的学习笔记—(牛客JZ50)

&#x1f600;前言 在处理字符串时&#xff0c;寻找第一个只出现一次的字符是一项常见的任务。本文将探讨几种有效的解法&#xff0c;包括使用 HashMap 和位集&#xff08;BitSet&#xff09;。 &#x1f3e0;个人主页&#xff1a;尘觉主页 文章目录 &#x1f970;第一个只出现…

来个Oracle一键检查

启停、切换、升级、网络改造等场景下&#xff0c;需要对数据库有些基本检查操作&#xff0c;确认当前是否运行正常&#xff0c;主打一个简单和一键搞定。 #!/bin/bash## 实例个数 告警日志 实例状态 会话 活动会话 锁 集群状态 服务状态 磁盘空间 侦听日志 ## linux vmstat 2 …