Java后台实现拖拽树状控件排序的数据持久化操作

背景

项目中有一个需求是创建一些树状多层级的目录,这些目录为了方便还要支持随时可编辑顺序。技术选型上,使用了ElementUI的可拖拽节点树即可实现拖拽节点编辑,技术难度是在后端对拖拽后的顺序重排序持久化上。

设计思路

  1. 添加节点,默认排序位置为最后
  2. 删除节点,删除父节点时连带子节点一并删除,并且要对已有树排序进行重排序
  3. 获取数据时,需要将数据拼装成树
  4. 更新节点,除了更新名字好像没啥要更新了
  5. 拖拽时对变动的位置进行上移或下移,原理是变动排序数字,可放到更新节点里一起做,但我建议这个单独写一个方法,单一职责

详细设计

约定

  1. 根节点的parentId为0;
  2. 排序号由小到大;
  3. 新建时排序号为父节点下最大的排序;
  4. 前端会传输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);
}

查询节点

查询节点我们要把查询结果组装成树的结构,这就有很多种方法:

  1. sql语句递归查询(MySQL 8.0版本,语法复杂,维护困难)
  2. 按数据库节点层级查库(多次调库,指数增长)
  3. 一次查库,程序封装(不适用于数据量大的场景)
  4. 数据库创建存储结构(维护不便)

我这里用的是第三种方案

定义节点树类

@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);
	
}

总结

很久没练习过数据结构了,重新整理学习了一下,这只是一个大概的方案,可优化的地方很多,比如对归档的节点(已找完树结构的节点)做一个移除操作,能大幅度提升程序处理的速度,可考虑使用链表来实现。具体的细节可根据业务场景补充。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值