JeeSite 快速开发平台
官方网站
    • 平台简介
    • 架构特点
    • 技术选型
    • 功能介绍
    • 安装部署
    • 常见问题
    • 目录结构
    • 更新日志
    • Vue 文档
    • BPM 文档
    • Cloud 文档
V5 演示
Vue 演示
💖联系
  • 我要提意见、文档纠错
  • JeeSite 代码库, 请⭐Star关注
  • JeeSite Vue 代码库, 请⭐关注
  • Spring Cloud 微服务架构
  • JeeSite 手机端/移动端
  • AI + RAG + CMS 人工智能
  • Flowable 中国式工作流
  • OAauth2 统一认证服务器
  • 政务内外网环境中间件
  • 访问 ThinkGem 官方博客
  • 点击进入,下拉查看动态
官方网站
    • 平台简介
    • 架构特点
    • 技术选型
    • 功能介绍
    • 安装部署
    • 常见问题
    • 目录结构
    • 更新日志
    • Vue 文档
    • BPM 文档
    • Cloud 文档
V5 演示
Vue 演示
💖联系
  • 我要提意见、文档纠错
  • JeeSite 代码库, 请⭐Star关注
  • JeeSite Vue 代码库, 请⭐关注
  • Spring Cloud 微服务架构
  • JeeSite 手机端/移动端
  • AI + RAG + CMS 人工智能
  • Flowable 中国式工作流
  • OAauth2 统一认证服务器
  • 政务内外网环境中间件
  • 访问 ThinkGem 官方博客
  • 点击进入,下拉查看动态
  • 快速了解

    • 平台简介
    • 架构特点
    • 技术选型
    • 功能介绍
    • 开发规范
    • 目录结构
    • 参数配置
    • 树表设计
      • 一、引言
      • 二、树表字段说明
      • 三、下面以区域树表举例
        • 1、定义实体Entity
        • 2、定义业务层Service
      • 四、常用示例调用
        • 1、保存一条数据:
        • 2、获取当前节点最大编号:
        • 3、查找下级子节点:
        • 4、查找所有子节点:
        • 5、只查找一级和二级节点:
        • 6、排除叶子节点:
        • 7、当前层级排序:
        • 8、全部层级排序:
        • 9、显示当前节点的全名称:
        • 10、综合实例:
    • 前后分离版
    • 平台更新日志 🚀
  • 后端开发手册

  • Vue前端手册

  • 经典前端手册

  • 扩展功能专题

  • 云服务技术架构

  • 技术支持与服务

树结构、树表、树形结构表的设计方法和用法

# 一、引言

有些同仁对于 JeeSite 中的树表设计不太了解,本应简单的方法就可实现,却写了很多复杂的语句和代码,所以有了这篇文章。在 JeeSite 中的树表设计我还是相对满意的,这种设计比较容易理解,不会太依赖数据库的语法,对兼容多数据库比较好。相比网上大牛的左右值树设计简单了很多,并且可随时调换父节点,并级联更新所有子节点数据。

看下表字段说明我们发现除了父级节点外又多了很多辅助字段,这写字段的维护可能会稍微影响我们的插入和更新性能, 但是这将极大的简化了我们的查询,并不限深度。废话不多说,下面我们一同来就来看看都有哪些好处。

# 二、树表字段说明

字段名 说明
xxx_code 节点编码(xxx表示用户自定义名称)
xxx_name 节点名称(xxx表示用户自定义名称)
以下是树表关键字段:
parent_code 节点上级编码
parent_codes 节点所有上级编码(快速检索下级节点)
tree_sort 当前层级排序号(decimal类型)
tree_sorts 树节点的完整排序号,10位数字组成(快速整树排序)
tree_leaf 是否是末级,是否叶子节点(0:否,1:是,char类型)
tree_level 节点层次级别(从0开始,decimal类型,快速分级查询,根据层级缩进)
tree_names 节点的全名称(用“/”分隔,快速获取当前节点完整路径)
以下是树表可选字段:
status 节点状态(0:正常,1:删除,2:停用)
create_by 创建者用户编码
create_date 数据创建时间
update_by 更新者用户编码
update_date 数据更新时间

# 三、下面以区域树表举例

# 1、定义实体Entity

用户自定义的节点编码是area_code,节点名称是area_name,实体注解配置如下:

@Table(name="${_prefix}sys_area", alias="a", columns={
		@Column(includeEntity=DataEntity.class),
		@Column(includeEntity=TreeEntity.class),
		@Column(name="area_code", attrName="areaCode", label="区域代码", isPK=true),
		@Column(name="area_name", attrName="areaName", label="区域名称", queryType=QueryType.LIKE, isTreeName=true),
		@Column(name="area_type", attrName="areaType", label="区域类型"),
	}, orderBy="a.tree_sorts, a.area_code"
)
public class Area extends TreeEntity<Area> {
	private String areaCode;		// 区域代码
	private String areaName;		// 区域名称
	private String areaType; 		// 区域类型(1:国家;2:省份、直辖市;3:地市;4:区县)
	// get set 省略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. Column 注解中的 includeEntity=TreeEntity.class 则自动导入树表关键字段配置(parent_code...)
  2. Column 注解中的 includeEntity=DataEntity.class 则自动导入树表可选字段配置(create_by...)
  3. 继承 TreeEntity 类就会自动拥有树表关键字段和可选字段的属性(parent_code...、create_by...)
  4. 节点编码 Column 必须设置 isPk=true 来确定是一个唯一主键
  5. 节点名称 Column 必须设置 isTreeName=true 来确定是一个节点名称,用来生成 tree_names 的值

# 2、定义业务层Service

下面我们介绍怎么来通过API操作这张表

@Service
@Transactional(readOnly=true)
public class AreaService extends TreeService<AreaDao, Area> {

}
1
2
3
4
5

该类继承了 TreeService,类的内容可以什么都不用写,就拥有了树表的增删改查等方法,以及树表维护的关键字段和可选字段的生成与更新。

下面我们就来展示下 TreeService 的方法:

@Transactional(readOnly=true)
public abstract class TreeService<D extends TreeDao<T>, T extends TreeEntity<T>> {

	/**
	 * 获取单条数据
	 * @param entity id
	 */
	@Override
	public T get(T entity) 
	
	/**
	 * 根据父节点获取子节点最后一条记录
	 * @param entity parentCode
	 */
	public T getLastByParentCode(T entity)
	
	/**
	 * 列表查询数据
	 * @param entity
	 */
	@Override
	public List<T> findList(T entity)
    
	/**
	 * 查询列表总数
	 * @param entity
	 */
	@Override
	public long findCount(T entity)
	
	/**
	 * 保存数据(插入或更新)
	 * 实现自动保存字段:所有父级编号、所有排序号、是否是叶子节点、节点的层次级别等数据
	 * 实现级联更新所有子节点数据:同父级自动保存字段
	 */
	@Override
	@Transactional(readOnly=false)
	public void save(T entity)
	
	/**
	 * 更新当前节点排序号
	 */
	@Transactional(readOnly=false)
	public void updateTreeSort(T entity)
	
	/**
	 * 预留接口事件,更新子节点
	 * @param childEntity 当前操作节点的子节点
	 * @param parentEntity 当前操作节点
	 */
	protected void updateChildNode(T childEntity, T parentEntity)
	
	/**
	 * 更新状态(级联更新父节点的tree_leaf字段)
	 * @param entity
	 */
	@Override
	@Transactional(readOnly=false)
	public void updateStatus(T entity) 
	
	/**
	 * 删除数据(级联删除子节点和父节点的tree_leaf字段)
	 * @param entity
	 */
	@Override
	@Transactional(readOnly=false)
	public void delete(T entity) 
	
	/**
	 * 修正本表树结构的所有父级编号
	 * 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段
	 */
	@Transactional(readOnly=false) // 可读取未提交数据
	public void fixTreeData()
	
	/**
	 * 按父级编码修正树结构的所有父级编号
	 * 包含:数据修复(parentCodes、treeLeaf、treeLevel、treeSorts、treeNames)字段
	 */
	@Transactional(readOnly=false) // 可读取未提交数据
	public void fixTreeData(String parentCode)
	
	/**
	 * 修正指定节点及下级节点的所有父级编号(这是个递归程序)
	 */
	private void fixTreeData(List<T> list, String parentCode, String parentCodes, String treeSorts, String treeNames)
	
	/**
	 * 将不同级别无序的树列表进行排序,前提是sourcelist每一级是有序的<br>
	 * 举例如下:<br>
	 * 	List<T> targetList = ListUtils.newArrayList();<br>
	 * 	List<T> sourceList = service.findList(entity);<br>
	 * 	service.listTreeSort(targetList, sourceList, T.ROOT_CODE);<br>
	 * @param sourceList 源数据列表
	 * @param targetList 目标数据列表
	 * @param parentCode 目标数据列表的顶级节点
	 */
	public void listTreeSort(List<T> sourceList, List<T> targetList, String parentCode)

	/**
	 * 将简单列表code,parentCode转换为嵌套列表形式[code,childList[code,childList[...]]]<br>
	 * 举例如下:<br>
	 * 	List<T> targetList = ListUtils.newArrayList();<br>
	 * 	List<T> sourceList = service.findList(entity);<br>
	 * 	service.convertChildList(targetList, sourceList, T.ROOT_CODE);<br>
	 * @param sourceList 源数据列表
	 * @param targetList 目标数据列表
	 * @param parentCode 目标数据列表的顶级节点
	 */
	public void convertChildList(List<T> sourceList, List<T> targetList, String parentCode)
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

# 四、常用示例调用

# 1、保存一条数据:

Area area = new Area();
area.setIsNewRecord(true);    // 代表新增还是更新
area.setParentCode('370000'); // 上级编码
area.setAreaCode('371000');   // 节点编码(唯一)
area.setAreaName('济南市');   // 节点名称
area.setTreeSort(1000);       // 本级排序号
areaService.save(area);
1
2
3
4
5
6
7

除了一些用户输入字段,其余辅助字段有save方法自动维护,调用者无需关心。

# 2、获取当前节点最大编号:

Area where = new Area();
where.setParentCode("370000");
Area last = areaService.getLastByParentCode(where);
System.out.println(last.getAreaCode());
1
2
3
4

# 3、查找下级子节点:

SQL:

select a.* from js_sys_area a where a.area_code='370000'
1

API:

Area where = new Area();
where.setParentCode("370000");
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4

# 4、查找所有子节点:

SQL:

select a.* from js_sys_area a where (a.area_code='370000' or a.parent_codes like '0,370000,%')
1

有的小伙伴可能会问 like 会影响性能,其实不然,右 like 是支持索引查询的。

API:

Area where = new Area();
SqlMap sqlMap = where.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = where.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getWhere().andBracket("area_code", QueryType.EQ, "370000", 1)
        .or("parent_codes", QueryType.RIGHT_LIKE, "0,370000,", 2).endBracket();
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4
5
6
7

# 5、只查找一级和二级节点:

SQL:

select a.* from js_sys_area a where a.tree_level <= 1
1

API:

Area where = new Area();
SqlMap sqlMap = where.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = where.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getWhere().and("tree_level", QueryType.LTE, 1);
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4
5
6

# 6、排除叶子节点:

SQL:

select a.* from js_sys_area a where a.tree_leaf = '0'
1

API:

Area where = new Area();
where.setTreeLeaf("0");
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4

# 7、当前层级排序:

SQL:

select a.* from js_sys_area a where a.parent_code = '0' order tree_sort asc
1

API:

Area where = new Area();
where.setParentCode("0");
SqlMap sqlMap = where.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = where.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getOrder().setOrderBy("a.tree_sort asc");
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4
5
6
7

# 8、全部层级排序:

SQL:

select a.* from js_sys_area a where (a.area_code='370000' or a.parent_codes like '0,370000,%') order tree_sorts asc
1

API:

Area where = new Area();
SqlMap sqlMap = where.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = where.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getWhere().andBracket("area_code", QueryType.EQ, "370000", 1)
        .or("parent_codes", QueryType.RIGHT_LIKE, "0,370000,", 2).endBracket();
sqlMap.getOrder().setOrderBy("a.tree_sorts asc");
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4
5
6
7
8

# 9、显示当前节点的全名称:

SQL:

select a.tree_names from js_sys_area a where a.area_code='370000'
1

API:

Area where = new Area();
where.setAreaCode("370000");
Area area = areaService.get(where);
System.out.println(area.getTreeNames());
1
2
3
4

# 10、综合实例:

查询编号为370000,及所有下级的数据,并只查询2级数据,并按照升序排序

SQL:

select a.* from js_sys_area a where (a.area_code='370000' or a.parent_codes like '0,370000,%') and a.tree_level <= 1 order by a.tree_sorts
1

API:

Area where = new Area();
SqlMap sqlMap = where.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = where.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getWhere().andBracket("area_code", QueryType.EQ, "370000", 1)
        .or("parent_codes", QueryType.RIGHT_LIKE, "0,370000,", 2).endBracket();
sqlMap.getWhere().and("tree_level", QueryType.LTE, 1);
sqlMap.getOrder().setOrderBy("a.tree_sorts asc");
List<Area> list = areaService.findList(where);
System.out.println(list);
1
2
3
4
5
6
7
8
9
进入 JeeSite 源码仓库页面,点击右上角 ⭐ Star 加星关注。

← 参数配置 前后分离版→

联系我们:s.jeesite.com  |  微信号:jeesitex  |  邮箱:jeesite@163.com
© 2013-2025 济南卓源软件有限公司 版权所有 | Theme Vdoing

请关注 JeeSite 微信公众号,了解最新动态

  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式