背景
项目中有一个需求是创建一些树状多层级的目录,这些目录为了方便还要支持随时可编辑顺序。技术选型上,使用了ElementUI的可拖拽节点树即可实现拖拽节点编辑,技术难度是在后端对拖拽后的顺序重排序持久化上。
设计思路
- 添加节点,默认排序位置为最后
- 删除节点,删除父节点时连带子节点一并删除,并且要对已有树排序进行重排序
- 获取数据时,需要将数据拼装成树
- 更新节点,除了更新名字好像没啥要更新了
- 拖拽时对变动的位置进行上移或下移,原理是变动排序数字,可放到更新节点里一起做,但我建议这个单独写一个方法,单一职责
详细设计
约定
- 根节点的parentId为0;
- 排序号由小到大;
- 新建时排序号为父节点下最大的排序;
- 前端会传输Node节点信息到后端
实体类Node
这是最基本的节点要素,数据库中也要创建与之对应的字段。
@Data
public class Node {
private Integer id; // 主键
private Integer parentId; // 父节点
private Integer sort; // 排序
}
添加节点
添加节点需要获得新建节点在所在父节点下的最大排序值,根节点是parentId为0的节点
public void addNode(Node node) {
node.setSort(getMaxSortFromParentId(node.getParentId()) + 1);
nodeService.addNode(node);
}
public int getMaxSortFromParentId(Integer parentId) {
// select count(1) from node where parent_id = #{parentId}
....
}
删除节点
删除节点需要考虑的是,删除之后,原先节点下面的节点排序号要 - 1。若删除父节点,其子节点是删除或者放置外层。这里只做排序号更新操作。不考虑子节点的问题,但这是不合规的,实际开发中一定要处理这个问题。
public void deleteNode(Node node) {
// 1. 直接删除节点
nodeService.removeById(node.getId());
// 2. 更新往下的排序号
updateSort(node);
}
public void updateSort(Node node) {
// update node set sort=sort-1 where parent_id = #{parentId} and sort >= #{sort}
.....
}
更新节点
这个真没啥内容,能在这个接口更新的都是一些业务数据,比如名称
public void updateNode(Node node) {
nodeService.update(node);
}
查询节点
查询节点我们要把查询结果组装成树的结构,这就有很多种方法:
- sql语句递归查询(MySQL 8.0版本,语法复杂,维护困难)
- 按数据库节点层级查库(多次调库,指数增长)
- 一次查库,程序封装(不适用于数据量大的场景)
- 数据库创建存储结构(维护不便)
我这里用的是第三种方案
定义节点树类
@Data
public class NodeTree {
private Integer id;
private Integer parent_id;
private Integer sort;
private List<NodeTree> childNodes;
}
实现业务方法
public List<NodeTree> getNodeTree() {
// 查询数据库中全部的节点, 数据库做了排序等会就不用排序了
// select * from node order by sort asc;
List<Node> nodes = nodeService.qryNodeTree();
List<NodeTree> results = new ArrayList<>(); // 返回结果集
for (Node n : nodes) {
// 没有父节点就是根节点,从根节点出发去获取子节点
if (StringUtils.hasText(node.getParentId())) {
NodeTree root = new NodeTree();
BeanUtils.copyProperties(n, root);
findChild(root, nodes);
results.add(root);
}
}
return results;
}
public NodeTree findChild(NodeTree root, List<Node> nodes) {
for (Node n : nodes) {
// 如果当前遍历的节点的父id等于根节点id则执行操作
if (n.getParentId() != null && n.getParentId().equals(String.valueOf(root.getId())) {
// 如果未创建子节点集合则创建
if (root.getChildNodes() == null) root.setChildNodes(new ArrayList<>());
// 往下递归
root.getChildNodes().add(findChild(n, nodes));
}
}
return root;
}
拖拽时刷新排序号
前端的拖拽事件回调方法获得拖拽后的节点信息,回传到后端
若子节点被拖拽到最外层成为根节点时,前端约定要设置ParentId为0
@Transactional
public void updateSort(Node node) {
// 获得旧的节点信息
Node oldNode = nodeService.getById(node.getId());
// 若同层级内拖拽,则不需要变更根节点
if (oldNode.getParentId().equals(node.getParentId())) {
// 同级上移
if (oldNode.getSort() > node.getSort()) {
// 旧位置与新位置之间的元素要往下移一位
// update node set sort=sort+1 where parent_id = #{parentId} and sort >= #{newSort} and sort < #{oldSort}
nodeService.peerUpSort(node.getParentId(), node.getSort(), oldNode.getSort());
}
// 同级下移
if (oldNode.getSort() < node.getSort()) {
// 旧位置与新位置之间的元素上移一位
// update node set sort=sort-1 where parent_id = #{parentId} and sort <= #{newSort} and sort > #{oldSort}
nodeService.peerDownSort(node.getParentId(), node.getSort(), oldNode.getSort());
}
} else {
// 非同级,需要切换父节点,原父节点移出位置往下元素上移一位,新父节点移入位置往下元素下移一位
// 新父节点元素下移
// update node set sort=sort+1 where parent_id = #{parentId} and sort >= #{newSort}
nodeService.upSort(node.getParentId(), node.getSort());
// 原父节点元素上移
// update node set sort=sort-1 where parent_id = #{parentId} and sort > #{oldSort}
nodeService.downSort(oldNode.getParentId(), oldNode.getSort());
}
// 保存节点信息
nodeService.updateById(node);
}
总结
很久没练习过数据结构了,重新整理学习了一下,这只是一个大概的方案,可优化的地方很多,比如对归档的节点(已找完树结构的节点)做一个移除操作,能大幅度提升程序处理的速度,可考虑使用链表来实现。具体的细节可根据业务场景补充。