前言
博主开始写这个系列主要还是因为,现在越来越多的程序员去学习新技术,大数据、分布式、微服务、区块链等方向的东西,但在组合业务系统的时候,又能很明显的发现,这些同学的业务开发能力太差,因博主以前比较懒,所以没留太多文章,目前准备整理并完善一套适用于业务系统开发的通用模块和日常容易遇到问题的开发内容。
目前正考虑从数据封装、通用返回、校验、session、工作流等若干方向完善,若有疑惑和需要交流的,希望大家能一起添加。
摘要
树形结构数据之于软件系统应该说是一个不可避免的数据格式,例如菜单、组织架构、关系等等。作者从业做过大大小小的软件几十种,将以自己的经验总结出主要和通用模式下的使用情况,方便不懂和小白同学了解和使用。
并会做对应的包装,大家可以直接拿去用。
文章大约一万字,有时间的可以慢慢看,没时间直接拿工具类去照着调用,有问题来聊。
总结
习惯性把总结写在前面。
- 树形结构数据,一定要有上下级关系的字段,不然无法构成树形,这里也有数据库表自关联的叫法;
- 做查询的时候,高效率一定只查1-2次数据库,也有根据SQL排序完成封装的方法,但排序有限制,不是一个通用处理的好方式;
- 文中所述方法只会查询2次数据库,后台会通过递归完成封装;
- 对于不使用包装类型,可以直接用Map封装,对于考虑顺序的,请在查询就指定清楚顺序,因为封装是顺序的;
- 核心是要理解,递归方法中,一直是在方法体内自己调用自己,并且传递进去的参数是有动态的;
完整流程
首先树形结构数据是有一个上下级关系的字段,本文统称为parent_id,我们也将该类数据叫做自关联表数据。
图1描述了一个自关联表内的数据内容,上图通过红框标注了ID,通过红色箭头表达了上下级关系,而通过蓝色箭头表达了三级关系。本文涉及的表叫组织关系表,统称为t_org。
若下图自关联要查询一个上下级关系的数据,如:查询制造部的下一级组织
以上SQL就是自关联表的查询,这里t1表替代的就是制造部,t2就是它的下级组织。
若查询要求变更为:查询制造部下级所有的组织
这样来看,倒是查询了下面的所有部门,但是实际上这里的只查询了下面两级的部门,如果有三级呢?四级呢?曾经博主就见过这些在路上越走越艰难的人。
- 一个同事写的代码,查询组织关系SQL写了5个自关联,因为有5级下级,这样写的代码,前端怎么去封装数据呢,for循环嵌套查询封装,写了4个。。 。后面自己又想了个排序的办法,把最子集的组织放在最上,形成一个倒序查询的内容,然后只写一个for循环封装,但是问题来了,如果有排序规则要求在里面,这个封装可能就会失败了,如果有某个下级组织在查询时排序到了上级的后面,那样按顺序封装是装不进去了。
- 还有同事这样写,直接来5个for循环,每个循环里查询一个SQL,从第一级组织查询起,后面拿到当前查询的ID传入查询出它的所有下一级,然后循环继续查,项目完成后,点开组织关系功能,后台大概查询了200次,虽然重复查询的SQL有预编译缓存,但是这样low不low,然后扩展性好不好?
为什么不用递归呢???答案是很多小伙伴不太会,对于学习java基础时的那些冒泡、快速排序等可能还有人记得,但是这些递归什么的,数学逻辑不好的同学确实转不清楚。。。
记住一个方法,当存在数据上下级关系,且不知道有多少级别的时候,递归是个不错的办法,可扩展性、易移植性、高效可读性明显强了不止一个档位。
为了保证封装的功能具有多平台的移植性,这里封装了一个实体,如果字段不对应,没关系,SQL查询改别名,改实体的属性都可以。
实体部分
Java开发里有实体部分的内容,如果有实体,对操作数据来说,可能在整套程序里有一个统一的输入输出,相比其他动态参数名的方式更具有准确性和可读性,这里需要说明几点:
- 完成数据库表的属性映射,如果字段名不匹配,可以自行修改实体对象Tree,其中必要字段为id、parent_id、text、children(children本字段是代码包装部分,不是数据库查询出来的);
- 清楚字段绑定到Tree的什么属性上了,children是一个List对象,
代码如下:
package com.visket.demo;
import java.io.Serializable;
import java.util.List;
/**
* 通用树形结构数据封装
* @author around
* @email [email protected]
* @date 2018-4-26
*/
public class Tree implements Serializable {
private static final long serialVersionUID = 5350719828301693594L;
/** 主键 */
private String id;
/** 上级ID */
private String parent_id;
/** 显示文本 */
private String text;
/** 对应URL */
private String url;
/** 子集 */
private List<Tree> children;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getParent_id() {
return parent_id;
}
public void setParent_id(String parent_id) {
this.parent_id = parent_id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<Tree> getChildren() {
return children;
}
public void setChildren(List<Tree> children) {
this.children = children;
}
@Override
public String toString() {
return "Tree [id=" + id + ", parent_id=" + parent_id + ", text=" + text + ", url=" + url + ", children="
+ children + "]";
}
}
递归逻辑
递归逻辑实际上就是一段在符合条件内不停调用自己,当找到尽头或满足某些条件停止的情况下做一级一级的返回,一直返回到第一级,网上找了个图,简单看看就好。
按照这个情况,根据上述的Tree实体,我们做了一个简单粗暴的查询:
select id, name as text, parent_id from t_org order by name
这个是最简单的SQL了,直接查询所有组织,因为跟Tree对应不上text,给name加了别名,各位同学自己的业务有冻结禁用之类的时,记得把该加的where条件加上。有了所有符合条件的组织信息之后,我们只需要对其进行递归封装即可。
这里还有一个问题,就是树形结构数据是有一个或若干个顶部数据的,所以进行递归时,我们得告诉 程序,谁是顶部组织,博主这里的顶部数据是where parent_id is null
(如上图打钩),也就是说这里要将哪个数据是头查出来,以便于后面封装有个入口,不然查询了所有记录,从哪里开始封装都不知道。
select id from t_org where parent_id is null
封装逻辑
先上一部分代码,功能主要是讲前一个步骤的2个SQL查询的数据分别带入,之后完成根级节点数据筛选,并从此处开始调用递归方法getChildrenNode()
实现对根级节点的children属性封装数据。
- list = 符合条件的结果集
- parent_id = 根级节点ID
/**
* 从list中找到根级节点数据,并开始完成封装根级节点的children属性
* @param list 查询到的所有数据
* @param parent_id 根级节点的ID
* @return List<Tree> 包装完毕后的树型结构
*/
public static List<Tree> getTree(List<Tree> list, String parent_id) {
//封装返回的内容
List<Tree> tree = Lists.newArrayList();
//迭代所有节点
for (Tree treeNode : list) {
//找到根级节点子集,若返回结果集需要有根级节点,那么用parent_id.equals(treeNode.getId())
if (parent_id.equals(treeNode.getPid())) {
//开始找根级节点的子集数据,调用getChildrenNode()方法,实际递归也是这个方法
treeNode.setChildren(getChildrenNode(list, treeNode.getId()));
tree.add(treeNode);
}
}
return tree;
}
之后上递归方法,开始封装各个递归方法内的结果集的children。
/**
* 迭代封装子集数据,不依赖于排序整合
* @param list 全部数据集合
* @param parent_id 上级ID
* @return List<Tree> 返回的是当前parent_id下的所有子集
*/
public static List<Tree> getChildrenNode(List<Tree> list, String parent_id) {
ArrayList<Tree> childrenNode = new ArrayList<>();
//迭代数据
for (Tree treeNode : list) {
//根据传入的上级parent_id,找到匹配的下级对象
if (parent_id.equals(treeNode.getPid())) {
//在当前下级对象,将其ID当做继续查找下级的parent_id传入递归调用找自己的下级组织
treeNode.setChildren(getChildrenNode(list, treeNode.getId()));
childrenNode.add(treeNode);
}
}
return childrenNode;
}
实际上递归方法没有那么难,只是要确定几个要素,根据什么条件,怎么回归,递归方法需要哪些不断可迭代传递的参数,就像上述方法中的parent_id
一样,它的值时不断在变化的,一直在将匹配到的对象的id转换为parent_id继续找当前对象的子集,那么当前对象的id也就自动转换为子集对象的parent_id了。
理解了这块,递归就不难了。
使用方法时,直接调用getTree()
即可。后附完整工具类方法,tree类数据应该不会难住看官了。
通用模式
好吧,针对于还不懂的同学,提供一个通用封装,简单任性,适用于所有情况,根据说明提供对应参数即可,下面上局部代码,后面上完整包装工具类方法,包含之前的内容。
下面的这段通用代码,说通用,是因为,博主不在使用特殊包装类型,直接使用的List<Map<String, Object>>
完成对其的封装,这样只要在你的查询SQL返回时,获取的结果集类型为List<Map<String, Object>>
即可,这个类型也是符合几乎所有的列表数据的格式。
/**
* 查询获取上下级封装,多用于分类树、菜单、字典、权限等;
* 要求主键名称="id",可自定义父级ID字段、子集封装的字段名等;
* @param list
* @param idField 主键字段key
* @param parentField 父级字段key
* @param parent_id 父级ID
* @param childrenField 子集存储Key
* @return
*/
public static List<Map<String, Object>> getTreeByMap(List<Map<String, Object>> list,
String idField, String parentField, String parent_id, String childrenField) {
//封装返回的内容
List<Map<String, Object>> tree = Lists.newArrayList();
//迭代所有节点
for (Map<String, Object> treeNode : list) {
//找到根级节点
if (parent_id.equals((String)treeNode.get(parentField))) {
treeNode.put(childrenField, getChildrenByMap(list, idField, parentField,
treeNode.get(idField).toString(), childrenField));
tree.add(treeNode);
}
}
return tree;
}
/**
* 迭代封装子集数据,不依赖于排序整合
* @param list
* @param idField 主键字段key
* @param parentField 父级字段key
* @param parent_id 父级ID
* @param childrenField 子集存储Key
* @return
*/
public static List<Map<String, Object>> getChildrenByMap(List<Map<String, Object>> list,
String idField, String parentField, String parent_id, String childrenField) {
ArrayList<Map<String, Object>> childrenNode = new ArrayList<Map<String, Object>>();
//迭代数据
for (Map<String, Object> treeNode : list) {
//查找当前下级并封装
if (parent_id.equals((String)treeNode.get(parentField))) {
treeNode.put(childrenField, getChildrenByMap(list, idField, parentField,
treeNode.get(idField).toString(), childrenField));
childrenNode.add(treeNode);
}
}
return childrenNode;
}
总结一下使用通用方法的步骤:
- 准备两个SQL,一个查询对应表符合要求的所有结果集返回类型为
List<Map<String, Object>>
,另一个为根级节点的ID,返回类型为String,如果你数据库表的主键不是字符类型,那么改上述方法的传参parent_id类型为对应类型; - 先查询2个SQL,然后调用封装方法
getTreeByMap()
,按照注释传递对应参数值 - 查看结果输出,如果不是预期结果,请断点调试看看
如有问题,欢迎交流。
附完整的代码工具类:
题外话,没有这个jar包的import com.google.common.collect.Lists
方法会报错,不导入包可以自己new List。
package com.yizhi.tqmis.common.module;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
/**
* 自定义实现树形菜单的动态绑定,完成上下级封装
* @author around
* @email [email protected]
* @date 2018-3-22
*/
public class TreeUtils {
/**
* 从list中找到根级节点数据,并开始完成封装根级节点的children属性
* @param list 查询到的所有数据
* @param parent_id 根级节点的ID
* @return List<Tree> 包装完毕后的树型结构
*/
public static List<Tree> getTree(List<Tree> list, String parent_id) {
//封装返回的内容
List<Tree> tree = Lists.newArrayList();
//迭代所有节点
for (Tree treeNode : list) {
//找到根级节点子集,若返回结果集需要有根级节点,那么用parent_id.equals(treeNode.getId())
if (parent_id.equals(treeNode.getPid())) {
//开始找根级节点的子集数据,调用getChildrenNode()方法,实际递归也是这个方法
treeNode.setChildren(getChildrenNode(list, treeNode.getId()));
tree.add(treeNode);
}
}
return tree;
}
/**
* 迭代封装子集数据,不依赖于排序整合
* @param list 全部数据集合
* @param parent_id 上级ID
* @return List<Tree> 返回的是当前parent_id下的所有子集
*/
public static List<Tree> getChildrenNode(List<Tree> list, String parent_id) {
ArrayList<Tree> childrenNode = new ArrayList<>();
//迭代数据
for (Tree treeNode : list) {
//根据传入的上级parent_id,找到匹配的下级对象
if (parent_id.equals(treeNode.getPid())) {
//在当前下级对象,将其ID当做继续查找下级的parent_id传入递归调用找自己的下级组织
treeNode.setChildren(getChildrenNode(list, treeNode.getId()));
childrenNode.add(treeNode);
}
}
return childrenNode;
}
/**
* 查询获取上下级封装,多用于分类树、菜单、字典、权限等;
* 要求主键名称="id",可自定义父级ID字段、子集封装的字段名等;
* @param list
* @param idField 主键字段key
* @param parentField 父级字段key
* @param parent_id 父级ID
* @param childrenField 子集存储Key
* @return
*/
public static List<Map<String, Object>> getTreeByMap(List<Map<String, Object>> list,
String idField, String parentField, String parent_id, String childrenField) {
//封装返回的内容
List<Map<String, Object>> tree = Lists.newArrayList();
//迭代所有节点
for (Map<String, Object> treeNode : list) {
//找到根级节点
if (parent_id.equals((String)treeNode.get(parentField))) {
treeNode.put(childrenField, getChildrenByMap(list, idField, parentField,
treeNode.get(idField).toString(), childrenField));
tree.add(treeNode);
}
}
return tree;
}
/**
* 迭代封装子集数据,不依赖于排序整合
* @param list
* @param idField 主键字段key
* @param parentField 父级字段key
* @param parent_id 父级ID
* @param childrenField 子集存储Key
* @return
*/
public static List<Map<String, Object>> getChildrenByMap(List<Map<String, Object>> list,
String idField, String parentField, String parent_id, String childrenField) {
ArrayList<Map<String, Object>> childrenNode = new ArrayList<Map<String, Object>>();
//迭代数据
for (Map<String, Object> treeNode : list) {
//查找当前下级并封装
if (parent_id.equals((String)treeNode.get(parentField))) {
treeNode.put(childrenField, getChildrenByMap(list, idField, parentField,
treeNode.get(idField).toString(), childrenField));
childrenNode.add(treeNode);
}
}
return childrenNode;
}
}