树形结构的表设计与Java接口实现

1、开发需求中的树形结构

树形结构在日常开发中很常见,如:

在这里插入图片描述

再比如:

在这里插入图片描述

还有:

在这里插入图片描述

2、表结构设计

这种树形结构,其 核心字段为parentId ,即父节点id。先看上面课程信息树形结构的表设计:

在这里插入图片描述

精髓就是:每条数据,id是它自己的名字,parentId是它爹的名字。根据parentId能知道它爹是谁,而它的id就是它儿子的parentId,WHERE parentId = Id就知道了它的儿子是谁。

MySQL表结构设计如下:

字段名 数据类型 解释 是否必须
id varchar 主键,唯一标识这条数据,也是它子节点的parentId
name varchar 分类中文名称,以后要展示在前端的信息
parentId varchar 核心字段,父节点id,它爹的名字
is_show tinyint 控制这条数据是否显示,就是逻辑删除的那个味儿 根据需求分析是否加,像全国省市区,自然不用
orderby int 排序字段,同层的节点,返回时谁先谁后 看需求,对节点展示有顺序要求时用
is_leaf tinyint 是否叶子节点,1是0否 看需求,方便后端的一个字段
nodelevel int 层级标识字段 看需求,像省市区树形,这个字段可以标识下1省级2市级3区级

3、接口实现代码

3.1 模型类与接口定义

定义返回给前端的vo类,核心属性childrenTreeNodes

//伪代码
//假设树形结构表的PO类叫TreePo
@Data
public class TreeVo extends TreePo{
    
    

	List<TreeVo> childrenTreeNodes;
}
//注意这里childrenTreeNodes类型为TreeVo,而不是TreePo,因为你儿子也有自己的儿子

随便定义个示意接口,免得突兀:

@Slf4j
@RestController
public class TreeInfocontroller{
    
    

	@GetMapping("/tree/info/list")
	public List<TreeVo> queryTreeNodes(){
    
    
		return null;
	}
}

3.2 Mapper层开发

这里以上面的行业分类树形表为例展示如何进行mapper层SQL的书写:

在这里插入图片描述

当树形结构的层级固定,比如都只有两级,此时使用表的自联结查询即可完成

# 假设表名是t_tree
# pid即核心字段parentId
SELECT
	 one.id one_id,
	 one.name one_name,
	 one.pid one_pid,
	 two.id two_id,
	 two.name two_name,
	 two.pid two_pid
FROM t_tree one
INNER JOIN t_tree two
ON one.id = two.pid
WHERE one.pid='根节点id';
# 有一条无意义数据,叫根节点,其子节点就是前端页面的第一级数据
# 有排序字段的话继续order by one.orderbyField,two.orderbyField

在这里插入图片描述

这就查出来了前两级:

在这里插入图片描述

当树形结构层级不固定,有的两级深、有的三级深,则应MySQL递归查询

WITH RECURSIVE tem_table AS(
	SELECT * FROM  t_tree one WHERE id='0'
	UNION ALL
	SELECT two.* FROM t_tree two INNER JOIN tem_table ON tem_table.id = two.pid
)

SELECT * FROM  tem_table ;
# 有排序需求时后面继续order by tem_table.id......
- tem_table是一个表名
- 使用UNION ALL 不断将每次递归得到的数据加入到表tem_table中
- select * from t_tree p where id= '0’即tem_table表中的初始数据是id=0的记录,即根节点
- 通过inner join tem_table ON tem_table.id = two.pid 找到id='0’的下级节点
- 最后select * from tem_table拿递归得到的所有数据

以上是MySQL8.0的写法,再补一个MySQL5.7版本的递归写法

//传入id,返回这个id所在节点的所有子节点
delimiter $$ 
drop function if exists get_child_list$$ 
create function get_child_list(in_id varchar(10)) returns varchar(1000) 
begin 
    declare ids varchar(1000) default ''; 
    declare tempids varchar(1000); 
 
    set tempids = in_id; 
    while tempids is not null do 
        set ids = CONCAT_WS(',',ids,tempids); 
        select GROUP_CONCAT(id) into tempids from t_tree where FIND_IN_SET(pid,tempids)>0;  
    end while; 
    return ids; 
end  
$$ 
delimiter ; 

两个版本的SQL递归实现参考这篇文章:https://blog.csdn.net/llg___/article/details/130908373

定义mapper接口:

public interface TreeMapper extends BaseMapper<TreePo> {
    
    
	//这里返回vo类型,方便后面代码。不过是查询出来的字段没有vo中的childrenTreeNodes,那就为null
    public List<TreeVo> selectTreeNodes(String id);
    
}


mapper.xml文件

<select id="selectTreeNodes" resultType="com.content.model.dto.TreeVo" parameterType="string">
    with recursive t1 as (
        select * from  t_tree p where  id= #{id}
        union all
        select t.* from t_tree t inner join t1 on t1.id = t.pid
    )
    select *  from t1 
</select>

3.3 Service层实现

到此,调用mapper接口能得到的数据结构是这样的,即List<TreeVo>,但childrenTreeNodes为null
在这里插入图片描述

在Service层,要做的就是把List<TreeVo>子节 的childrenTreeNodes属性处理好,也就是把节点的子节点找到,并一个个add到进其父的List<TreeVo> childrenTreeNodes属性。

//Service层接口
public interface TreeService {
    
    
  
    public List<TreeVo> queryTreeNodes(String id);
}


Service层实现类!!继递归查询后的又一个核心操作:

@Slf4j
@Service
public class TreeServiceImpl implements TreeService {
    
    

    @Autowired
    TreeMapper treeMapper;

    public List<TreeVo> queryTreeNodes(String id) {
    
    
        List<TreeVo> poList = treeMapper.selectTreeNodes(id);
    	//排除根节点后,将list转map,以备后面使用
   	    Map<String, TreeVo> mapTemp = poList.stream().filter(item->!id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
    	//定义最终要返回的list空集合
    	List<TreeVo> treeVoList = new ArrayList<>();
    	//依次遍历每个元素,排除根节点
    	poList.stream().filter(item->!id.equals(item.getId())).forEach(item->{
    
    
	    	//父节点是我们传入的id,即父节点是1,如1-1前端开发,那就塞进List
	        if(item.getParentid().equals(id)){
    
    
	            treeVoList.add(item);
	        }
	        //找到当前节点的父节点
	        TreeVo parentPo = mapTemp.get(item.getParentid());
	        if(parentPo!=null){
    
    
	            if(parentPo.getChildrenTreeNodes() ==null){
    
    
	                parentPo.setChildrenTreeNodes(new ArrayList<TreeVo>());
	            }
	            //下边开始往ChildrenTreeNodes属性的list集合中加入这个子节点
	            parentPo.getChildrenTreeNodes().add(item);
	        }
	    });
    return treeVoList;
    }

}

//实现思路总之就是,如果某条数据的pid是传入的id,则放到返回的集合中
//如果该数据的pid不是传入的id,那就找到其父节点,并将这个数据add到其父节点的List<TreeVo> childrenTreeNodes;

3.4、完善Controller层

@Slf4j
@RestController
public class TreeInfocontroller{
    
    
	
	@Autowierd
	TreeService treeService;
	
	@GetMapping("/tree/info/list")
	public List<TreeVo> queryTreeNodes(){
    
    
		return treeService.queryTreeNodes("1");
	}
}


返回部分结果:

{
"id" : "1-2",
"isLeaf" : null,
"isShow" : null,
"label" : "移动开发",
"name" : "移动开发",
"orderby" : 2,
"parentid" : "1",
"childrenTreeNodes" : [
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-2-1",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "微信开发",
                  "name" : "微信开发",
                  "orderby" : 1,
                  "parentid" : "1-2"
               },
               {
                  "childrenTreeNodes" : null,
                  "id" : "1-2-2",
                  "isLeaf" : null,
                  "isShow" : null,
                  "label" : "app开发",
                  "name" : "app开发",
                  "orderby" : 1,
                  "parentid" : "1-2"
               }
               ]
 }


4、另一种思路

最后说明下,并不是一遇见树形结构就要递归,具体怎么实现,要看具体需求。比如:

需求:返回一级二级节点信息给前端,不用返回二级节点的子节点信息

在这里插入图片描述

此时其实不用自连结查询也不用递归查,直接在mapper层定义一个根据pid查询所有子类List的方法,在service层调用,并将该方法的返回值set成vo的ChildrenTreeNodes即可。具体实现:

@Data
public class TreeVo extends TreePo{
    
    

	//此时,不考虑二级属性的子节点,
	//因此childrenTreeNodes类型为TreePo,不再用TreeVo
	List<TreePo> childrenTreeNodes;
}

Mapper层代码:

//Mapper接口
@Select("select * from tree where pid = #{pid}")
List<TreePo> selectByPid(String pid);

Controller和Service接口定义略过了,直接写Service层的接口实现的示意代码:

//传入根节点id,如1
public List<TreeVo> queryTreeNodes(String id){
    
    
	List<TreeVo> voList = new ArrayList<>();
	//一级信息,各省份数据到手
	List<TreePo> poList = treeMapper.selectByPid(id);
	for(TreePo po: poList){
    
    
		//由省份id查就是该省的所有市级信息
		List<TreePo> poTwoList = treeMapper.selectByPid(po.getId());
		TreeVo vo = new TreeVo();
		BeanUtils.copyProperties(po,vo);
		//二级信息set进childrenTreeNodes属性
		vo.setChildrenTreeNodes(poTwoList);
		voList.add(vo);
	}
	return voList;
	
}

猜你喜欢

转载自blog.csdn.net/llg___/article/details/130881120