目录
- 前言
- 1. 基本框架
- 2. 实战应用
前言
对应的每个子孙属于该父亲,这其实是数据结构的基础知识,那怎么划分怎么归属呢
对应的基本知识推荐如下:
- 【数据结构】树和二叉树详细分析(全)
- 【数据结构】B树和B+树的笔记详细诠释
1. 基本框架
最基本的树形结构节点,一个自身ID,一个父亲ID,一个孩子ID:
import java.io.Serializable;
import java.util.List;
public interface INode<T> extends Serializable {
Long getId();
Long getParentId();
List<T> getChildren();
default Boolean getHasChildren() {
return false;
}
}
其中ForestNodeManager
树形管理类主要表示如下:
nodeMap
存储节点,并提供了一些方法来操作节点和管理树状结构getRoot
方法用于获取合并后的树的根节点列表addParentId
方法用于标记尚未创建的父节点
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ForestNodeManager<T extends INode<T>> {
/**
* 使用 ImmutableMap 存储节点,其中键是节点的ID,值是节点本身。
* 这个映射是不可变的,通过调用 Maps.uniqueIndex(nodes, INode::getId) 来创建,其中 INode::getId 是获取节点ID的方法。
*
* parentIdMap: 使用 HashMap 存储尚未创建的父节点的ID。键是父节点的ID,值是一个占位对象(在这里是空字符串)。
*/
private final ImmutableMap<Long, T> nodeMap;
private final Map<Long, Object> parentIdMap = Maps.newHashMap();
/**
* 接受一个 List<T> 类型的参数 nodes,并使用 Maps.uniqueIndex 方法将其转换为 ImmutableMap 存储在 nodeMap 中
*/
public ForestNodeManager(List<T> nodes) {
this.nodeMap = Maps.uniqueIndex(nodes, INode::getId);
}
/**
* 接受一个 Long 类型的节点ID作为参数,然后尝试从 nodeMap 中获取对应ID的节点。
* 如果存在该节点,则返回节点;否则返回 null。
*/
public INode<T> getTreeNodeAt(Long id) {
return this.nodeMap.containsKey(id) ? (INode)this.nodeMap.get(id) : null;
}
/**
* 接受一个 Long 类型的父节点ID作为参数,将其添加到 parentIdMap 中。
* 这个方法用于标记尚未创建的父节点。
*/
public void addParentId(Long parentId) {
this.parentIdMap.put(parentId, "");
}
/**
* 创建一个空的 ArrayList 用于存储树的根节点。
* 使用 forEach 遍历 nodeMap 中的每个节点。
* 对于每个节点,如果其父节点ID为0(表示是根节点)或者父节点ID在 parentIdMap 中存在(即尚未创建的父节点),则将该节点添加到根节点列表中。
* 最后返回根节点列表。
*/
public List<T> getRoot() {
List<T> roots = new ArrayList();
this.nodeMap.forEach((key, node) -> {
if (node.getParentId() == 0L || this.parentIdMap.containsKey(node.getId())) {
roots.add(node);
}
});
return roots;
}
}
对于ForestNodeMerger
合并类:
包含树状结构节点的列表,根据节点之间的父子关系进行合并,最终返回合并后的树的根节点列表。
import java.util.List;
public class ForestNodeMerger {
public ForestNodeMerger() {
}
/**
* 对于每个节点,通过 getParentId() 方法获取其父节点的ID。
* 如果父节点ID不等于0(即有父节点),则尝试通过 forestNodeManager.getTreeNodeAt() 方法获取父节点。
* 如果成功获取到父节点,则将当前节点添加到父节点的子节点列表中。否则,说明父节点尚未被创建,需要通过 forestNodeManager.addParentId() 方法添加到待创建父节点的列表中。
*
* 最后,通过 forestNodeManager.getRoot() 方法获取合并后的树的根节点列表,并返回这个列表。
*/
public static <T extends INode<T>> List<T> merge(List<T> items) {
ForestNodeManager<T> forestNodeManager = new ForestNodeManager(items);
items.forEach((forestNode) -> {
if (forestNode.getParentId() != 0L) {
INode<T> node = forestNodeManager.getTreeNodeAt(forestNode.getParentId());
if (node != null) {
node.getChildren().add(forestNode);
} else {
forestNodeManager.addParentId(forestNode.getId());
}
}
});
return forestNodeManager.getRoot();
}
}
2. 实战应用
大多数本身Mapper 或者 Service基层都会帮我们实现增删改查,主要是XML文件对数据库的逻辑
本身就是MybatisPlus框架,推荐阅读:
- Springboot整合MybatisPlus的基本CRUD(全)
- MyBatis-plus从入门到精通(全)
与日常开发差不多,主要是上述中的方法如何套用!
假设要做一个菜单专栏,必须由这种树形结构来管理,好方便那个子类归属那个父类!
对应的类别如下:
@Data
@TableName("menu")
@ApiModel(value = "Menu对象", description = "Menu对象")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "主键")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 菜单父主键
*/
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "菜单父主键")
private Long parentId;
/**
* 菜单编号
*/
@ApiModelProperty(value = "菜单编号")
private String code;
/**
* 菜单名称
*/
@ApiModelProperty(value = "菜单名称")
private String name;
/**
* 菜单别名
*/
@ApiModelProperty(value = "菜单别名")
private String alias;
/**
* 请求地址
*/
@ApiModelProperty(value = "请求地址")
private String path;
/**
* 菜单资源
*/
@ApiModelProperty(value = "菜单资源")
private String source;
/**
* 排序
*/
@ApiModelProperty(value = "排序")
private Integer sort;
/**
* 菜单类型
*/
@ApiModelProperty(value = "菜单类型")
private Integer category;
/**
* 操作按钮类型
*/
@ApiModelProperty(value = "操作按钮类型")
private Integer action;
/**
* 是否打开新页面
*/
@ApiModelProperty(value = "是否打开新页面")
private Integer isOpen;
/**
* 备注
*/
@ApiModelProperty(value = "备注")
private String remark;
/**
* 是否已删除
*/
@TableLogic
@ApiModelProperty(value = "是否已删除")
private Integer isDeleted;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
Menu other = (Menu) obj;
if (Func.equals(this.getId(), other.getId())) {
return true;
}
return false;
}
}
对应与前端交互的VO类如下:public class MenuVO extends Menu implements INode<MenuVO>
,大部分这里都是这种写法!
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "MenuVO对象", description = "MenuVO对象")
public class MenuVO extends Menu implements INode<MenuVO> {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 父节点ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long parentId;
/**
* 子孙节点
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<MenuVO> children;
/**
* 是否有子孙节点
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Boolean hasChildren;
@Override
public List<MenuVO> getChildren() {
if (this.children == null) {
this.children = new ArrayList<>();
}
return this.children;
}
/**
* 上级菜单
*/
private String parentName;
/**
* 菜单类型
*/
private String categoryName;
/**
* 按钮功能
*/
private String actionName;
/**
* 是否新窗口打开
*/
private String isOpenName;
}
对应的懒加载实现类主要如下:
@Override
public List<MenuVO> lazyList(Long parentId, Map<String, Object> param) {
if (Func.isEmpty(Func.toStr(param.get("parentId")))) {
parentId = null;
}
return baseMapper.lazyList(parentId, param);
}
对应获取树形结构的实现类如下:
@Override
public List<MenuVO> tree() {
return ForestNodeMerger.merge(baseMapper.tree());
}
其中涉及Mapper的方法主要在xml文件中实现:
<select id="tree" resultMap="treeNodeResultMap">
select id, parent_id, name as title, id as "value", id as "key" from menu where is_deleted = 0 and category = 1
</select>