考虑下日常开发的实际使用所以整理了此文,建议了解mptt树先参考下基本介绍再来看此文.此外笔者也是第一次尝试实现这个算法,并且网上没有找到特别舒服的资料,因此写了此文,如有错误烦请指正,感谢.
1. 网上写这个的资料还挺少的,大多数还是写的存储过程,大多数场景下还是不用这个了,因此写个日常编码的形式
2. 其次大多数资料都是基于存在一个根节点的前提下去写的,我们平常用的业务场景可能根本就没有一个根节点,你可能说可以加一个,不对业务展示就行了,但是考虑到多租户又会有一些租户隔离的问题,总之确实能解,但是我想尝试能不能在设计层就给他解决了,本文使用的也是假设不存在根节点(或者说可以创建多个根节点的形式)
3. 另外一个大坑就是更新操作,网上都是一笔带过,因为更新可以看成删除和新增,确实如此,但是删除肯定不可能真删除(因为你只是移动了节点,你删了后再新增id如果变了就完了),而且我们希望新增能快一点,不是一个个节点的新增,因此也将更新单独写了一遍
每个节点有一个左右值,可以看成一条蛇从根节点左值开始,顺着最左树枝向下爬到末级节点后过渡到右值然后向上到汇集节点后兄弟节点继续重复上述动作,每次加1,最终爬一遍整棵树后回到根节点.
我们的项目要求实现多租户,我们实现的方式是表里加租户id,不需要多租户的可以不考虑这个字段
public class MpttNode {private String tenantId;private Long id;private String name;private Long parentId;private Integer level;private Integer mpttLeft;private Integer mpttRight;}
首先查询大多能找到资料且都是简单sql这里就不提了,最关键的一个接口就是查询某个节点下的子节点,即查询大于该节点左值小于该节点右值的节点
private List<MpttNode> findAllSubMpttNode(String tenantId, int left, int right) {return list(new QueryWrapper<MpttNode>().lambda().eq(MpttNode::getTenantId, tenantId).gt(MpttNode::getMpttLeft, left).lt(MpttNode::getMpttRight, right));}
首先写操作必须加锁, 基本就是表级别加锁,我这用分布式锁取代了,一个是方便操作,二是我们多租户没必要锁一整个表,这里的写操作也只有三种: 新增,更新,删除
//distribute lock for tenantId//这里锁的粒度不好控制,因为mptt本身是树级别的链式转化,那么涉及的子节点数量就不好判断// 比如假设新增的时候只锁父节点,但是删除其下的某个隔代节点时// 即便使用crabbing协议好像也只能从上往下锁,而无法锁住该节点后续的right值大于父节点right值的节点// 所以索性干脆锁整棵树了RLock lock = locker.lock(String.format(MPTT_KEY, tenantId);try {write(mpttNode);} finally {// release distribute locklock.unlock();}
因为这些sql用mybatis plus 不好表示,所以写成mapper了,后面调用的baseMapper都是这几个sql,这几个sql可以先不看,看到下面的代码实现后再回来找即可.
<update id="decrLeft">update mptt_treeset mptt_left = mptt_left - #{diff}where tenant_id = #{tenantId} and mptt_left > #{left}</update><update id="decrRight">update mptt_treeset mptt_right = mptt_right - #{diff}where tenant_id = #{tenantId} and mptt_right > #{right}</update><update id="incrLeft">update mptt_treeset mptt_left = mptt_left + #{diff}where tenant_id = #{tenantId} and mptt_left > #{incrBase}</update><update id="incrRight">update mptt_treeset mptt_right = mptt_right + #{diff}where tenant_id = #{tenantId} and mptt_right > #{incrBase}</update><select id="findMaxRight" resultType="java.lang.Integer">select max(mptt_right) from mptt_treewhere tenant_id = #{tenantId} and parent_id = #{parentId}</select>
public MpttNode createMpttNode(MpttNodeReq req) {MpttNode parentMpttNode = getOne(new QueryWrapper<MpttNode>().lambda().eq(MpttNode::getTenantId, TenantId()).eq(MpttNode::getId, ParentId()));final MpttNode mpttNode = new MpttNode(req);// set levelfinal int level = Optional.ofNullable(parentMpttNode).map(MpttNode::getLevel).orElse(0) + 1;mpttNode.setLevel(level);// 假设每次添加新节点都是往右侧添加// 取父节点下直接子节点的最右节点Integer maxRight = baseMapper.TenantId(), ParentId());// 如果 没有 最大的 right 的if (maxRight == null) {// 空树, parentId = -1if (parentMpttNode == null) {mpttNode.setMpttLeft(0);mpttNode.setMpttRight(1);save(mpttNode);} else {// 该 parent 没有任何子节点// 更新所有后继节点 + 2baseMapper.TenantId(), MpttLeft(), 2);baseMapper.TenantId(), MpttLeft(), 2);mpttNode.MpttLeft() + 1);mpttNode.MpttLeft() + 1);save(mpttNode);}} else {// 否则为同级情况// + 1作为左节点 + 2 作为右节点mpttNode.setMpttLeft(maxRight + 1);mpttNode.setMpttRight(maxRight + 2);baseMapper.TenantId(), maxRight, 2);baseMapper.TenantId(), maxRight, 2);save(mpttNode);}return mpttNode;}
public void deleteMpttNode(String tenantId, Long id) {// 校验是否关联简历// 查处所有子节点MpttNode mpttNode = getOne(new QueryWrapper<MpttNode>().lambda().eq(MpttNode::getTenantId, tenantId).eq(MpttNode::getId, id));List<MpttNode> allSubMpttNodeList = findAllSubMpttNode(tenantId, MpttLeft(), MpttRight());List<Long> relatedIds = new ArrayList<>();relatedIds.Id());relatedIds.addAll(allSubMpttNodeList.stream().map(MpttNode::getId).List()));Integer mpttLeft = MpttLeft();Integer mpttRight = MpttRight();int diff = mpttRight - mpttLeft + 1;// 删除当前节点,影响的是这条蛇往后爬的节点baseMapper.decrLeft(tenantId, mpttLeft, diff);baseMapper.decrRight(tenantId, mpttRight, diff);removeByIds(relatedIds);}
public MpttNode updateMpttNode(MpttNodeReq req) {// 仅更新名称之类的,不改变结构关系final MpttNode mpttNode = new MpttNode(req);MpttNode existOne = getOne(new QueryWrapper<MpttNode>().lambda().eq(MpttNode::getId, Id()).eq(MpttNode::getTenantId, TenantId()));if (ParentId().ParentId())) {updateById(mpttNode);return mpttNode;}// 先查出该节点以及改节点的子节点是哪些List<MpttNode> allSubMpttNode = TenantId(), MpttLeft(), MpttRight());MpttNode newParentNode = getOne(new QueryWrapper<MpttNode>().lambda().eq(MpttNode::getTenantId, TenantId()).eq(MpttNode::getParentId, ParentId()));// 相当于先删除 后新建Integer mpttLeft = MpttLeft();Integer mpttRight = MpttRight();int diff = mpttRight - mpttLeft + 1;// 该节点摘掉,后继节点肯定 decr 了baseMapper.TenantId(), mpttLeft, diff);baseMapper.TenantId(), mpttRight, diff);// 然后更新新的talentPool, 相当于在新 parent 下新增,然后改掉之前的 allSubTalentPool left right// 因为 mptt 本质是链式的,所以只要求出 变更前后 节点的左值变化,其所有子节点的左右值都应用这个变化即可// 取父节点下直接子节点的最右节点Integer maxRight = baseMapper.TenantId(), ParentId());// 如果 没有 最大的 right 的if (maxRight == null) {// 因为是更新,所以一定不是空树// 空树: maxRight == null && newParentNode == null// 就是说 maxRight == null 的时候 newParentNode 一定不等于null// 因为 newParentNode 为null, 只有为根节点的时候才存在// 而这种情况下,又只有 非根节点直接子节点 以外的节点迁移过来才算,根直接子节点再迁移相当于没改parentId,不参与更新了// 既然有这种节点,说明 非根节点直接子节点 必不为空,那么maxRight 必不等于 null//// 综上, newParentNode 这里一定不为 null,并且其不存在直接子节点mpttNode.MpttLeft() + 1);compareAndSetChange(mpttNode, existOne, allSubMpttNode);// 后续节点直接加diff即可baseMapper.TenantId(), MpttRight(), diff);// 注意的是该父节点添加新节点后其本身的右值也变化了,所以是 EQbaseMapper.TenantId(), MpttLeft(), diff);// 最后强刷算好的新位置updateById(mpttNode);if (CollectionUtils.isNotEmpty(allSubMpttNode)) {updateBatchById(allSubMpttNode);}} else {// 否则为同级情况// + 1作为左节点 + 2 作为右节点mpttNode.setMpttLeft(maxRight + 1);compareAndSetChange(mpttNode, existOne, allSubMpttNode);// 后续节点直接加diff即可baseMapper.TenantId(), maxRight, diff);// 注意的是该父节点添加新节点后其本身的右值也变化了,所以是 EQbaseMapper.TenantId(), maxRight, diff);// 最后强刷算好的新位置updateById(mpttNode);if (CollectionUtils.isNotEmpty(allSubMpttNode)) {updateBatchById(allSubMpttNode);}}return mpttNode;}private void compareAndSetChange(MpttNode mpttNode, MpttNode existOne, List<MpttNode> allSubMpttNode) {mpttNode.MpttLeft() + (MpttRight() - MpttLeft()));// 子节点应用这个 changeint change = MpttLeft() - MpttLeft();int changeLevel = Level() - Level();for (MpttNode subNode : allSubMpttNode) {subNode.MpttLeft() + change);subNode.MpttRight() + change);subNode.Level() + changeLevel);}}
本文发布于:2024-02-02 05:48:19,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170682410041764.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |