优雅全套式解决树形数据表结构的增删改查问题

引言:

在实际的开发过程,经常会使用到树形结构,我的数据库表设计一般都会有这三个字段

id(主键) parent_id(父节点编号) leaf(是否为叶子节点)

在这里插入图片描述

在这里插入图片描述

由于经常使用,故封装了一系列,全套式解决方法的工具类

构建树形结构

可以选择反射工具类hutool.ReflectUtil

import cn.hutool.core.collection.CollUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 构建树形结构工具类
 *
 * @author xzx
 * @date 2022/11/20
 */
public class TreeUtil {

    /**
     * 根据父节点的ID获取所有子节点
     *
     * @param list     平级的节点列表
     * @param parentId 传入的父节点ID 根节点默认0
     * @return String
     */
    public static <T> List<T> getChildPerms(List<T> list, int parentId) {
        List<T> returnList = new ArrayList<>();
        //遍历每一个菜单
        for (T t : list) {
            // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
            Integer pid = ReflectUtil.getFieldValue(t, "parentId");
            if (pid != null && pid.equals(parentId)) {
                //获取子菜单
                recursionFn(list, t);
                returnList.add(t);//添加一级菜单
            }
        }
        return returnList;
    }


    /**
     * 递归列表
     *
     * @param list 平级列表
     * @param t    当前节点
     */
    private static <T> void recursionFn(List<T> list, T t) {
        // 得到子节点列表
        List<T> childList = getChildList(list, t);
        if (CollUtil.isNotEmpty(childList)) {
            ReflectUtil.invokeSetter(t, "children", childList);
        }
        //遍历子节点,构建子节点的树状结构
        for (T tChild : childList) {
            //判断该节点是否还有子节点
            if (hasChild(list, tChild)) {
                recursionFn(list, tChild);
            }
        }
    }


    /**
     * 得到子节点列表
     * 判断list中的每一个节点的 parentId == t.id
     *
     * @param list 平级列表
     * @param t    父节点
     * @param <T>  泛型
     * @return t的子节点列表
     */
    private static <T> List<T> getChildList(List<T> list, T t) {
        List<T> tList = new ArrayList<>();
        Integer id = ReflectUtil.getFieldValue(t, "id");
        for (T n : list) {
            Integer parentId = ReflectUtil.getFieldValue(n, "parentId");
            if (Objects.equals(parentId, id)) {
                tList.add(n);
            }
        }
        return tList;
    }

    /**
     * 判断是否有子节点
     *
     * @param list 平级列表
     * @param t    父节点
     * @param <T>  泛型
     * @return
     */
    private static <T> boolean hasChild(List<T> list, T t) {
        return getChildList(list, t).size() > 0;
    }
}

【使用例子】

@Override
public ResponseResult getTreeList() {
	return ResponseResult.okResult(TreeUtil.getChildPerms(list(), 1));
}

增删改

然后再实际开发过程中,关于树形结构的节点的增删改,都可能影响到它的前驱节点的leaf【是否为叶子节点】,故又统一再服务层封装工具类

为了能够动态的调用Mabatis-Plus的save、removeById、queryWrapper,大量的运用了【hutool.ReflectUtil】反射工具类。

【要点】

  1. 在更新leaf的值时采用类似【CAS】的操作
  2. 校验一个节点是否拥有子节点【hasChildren】,sql语句类似“select xx from xxx where parentId == curNodeId limit 1”,重点在于【limit 1】操作,在MP的getOne()的实质是getList().get(0),而One()是需要保证唯一性,都不符合我们的要求,则使用last(“limit 1”), 将“limit 1”提取为常量
  3. 开启事务
package com.sztu.server.service;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.sztu.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
 * 处理树状结构的逻辑
 *
 * @author xzx
 * @date 2022/11/24
 */
@Component
@Slf4j
public class TreeService {


    /**
     * 添加一个节点
     *
     * @param node        节点
     * @param beanClass   类型
     * @param serviceImpl 业务实现类
     * @param <T>         泛型
     * @return boolean
     */
    @Transactional
    public <T> boolean addTreeNode(T node, Class<T> beanClass, Object serviceImpl) {
        // 1.save
        Boolean save = ReflectUtil.invoke(serviceImpl, "save", node);
        if (BooleanUtil.isFalse(save)) {
            return false;
        }
        // 2.parentNode has children : leaf [true] -> [false]
        return updateNodeLeaf(getParentId(node), serviceImpl, true, false);
    }

    /**
     * 移除一个节点
     *
     * @param node
     * @param beanClass
     * @param serviceImpl
     * @param <T>
     * @return
     */
    @Transactional
    public <T> boolean removeTreeNode(T node, Class<T> beanClass, Object serviceImpl) {
        // 0.judge
        if (hasChildren(node, serviceImpl)) {
            log.info("该节点存在孩子节点,不可以删除");
            return false;
        }
        // 1.remove
        boolean removeById = ReflectUtil.invoke(serviceImpl, "removeById", getId(node));
        if (BooleanUtil.isFalse(removeById)) {
            return false;
        }
        // 2.check node's parentNode hasChildren?
        if (BooleanUtil.isTrue(parentNodeHasChildren(node, serviceImpl))) {
            return true;
        }
        // noChildren : leaf [false] -> [true]
        return updateNodeLeaf(getParentId(node), serviceImpl, false, true);
    }

    /**
     * 更新一个节点
     *
     * @param node
     * @param <T>
     */
    @Transactional
    public <T> boolean updateTreeNode(T node, Class<T> beanClass, Object serviceImpl) {
        // 1.get parentId form database
        T oldNode = ReflectUtil.invoke(serviceImpl, "getById", getId(node));
        if (ObjUtil.isNull(oldNode)) {
            log.info("该节点不存在");
            return false;
        }
        int oldParentId = getParentId(oldNode);
        int newParentId = getParentId(node);
        // 2.update
        boolean updateById = ReflectUtil.invoke(serviceImpl, "updateById", node);
        if (BooleanUtil.isFalse(updateById)) {
            return false;
        }
        // 3.check
        if (oldParentId == newParentId) {
            return true;
        }
        // oldNode maybe become noChildren
        if (BooleanUtil.isFalse(hasChildren(serviceImpl, oldParentId))) {
            // set noChildren where hasChildren
            updateNodeLeaf(oldParentId, serviceImpl, false, true);
        }
        // set hasChildren where noChildren
        return updateNodeLeaf(newParentId, serviceImpl, true, false);
    }

    /**
     * 获取字段【id】的值
     *
     * @param node
     * @param <T>
     * @return
     */
    @Nullable
    private <T> int getId(T node) {
        return (int) ReflectUtil.getFieldValue(node, "id");
    }


    /**
     * 更新编号为id的节点的leaf属性
     *
     * @param id
     * @param serviceImpl
     * @param <T>
     * @return
     */
    private <T> boolean updateNodeLeaf(int id, Object serviceImpl, boolean oldValue, boolean newValue) {
        UpdateWrapper<T> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("id", id)
                .eq("leaf", oldValue) // 是叶子节点
                .set("leaf", newValue);
        return ReflectUtil.invoke(serviceImpl, "update", updateWrapper);
    }


    /**
     * 获取字段【parentId]值
     *
     * @param node
     * @param <T>
     * @return
     */
    @Nullable
    private <T> int getParentId(T node) {
        return (int) ReflectUtil.getFieldValue(node, "parentId");
    }

    /**
     * 判断当前节点的父节点是否还有孩子
     *
     * @param node
     * @param serviceImpl
     * @param <T>
     * @return
     */
    private <T> boolean parentNodeHasChildren(T node, Object serviceImpl) {
        return hasChildren(serviceImpl, getParentId(node));
    }

    /**
     * 判断当前节点是否还有孩子
     *
     * @param node
     * @param serviceImpl
     * @param <T>
     * @return
     */
    private <T> boolean hasChildren(T node, Object serviceImpl) {
        return hasChildren(serviceImpl, getId(node));
    }

    /**
     * 判断编号为id的节点是否还有孩子
     *
     * @param serviceImpl
     * @param id
     * @param <T>
     * @return
     */
    private <T> boolean hasChildren(Object serviceImpl, int id) {
        // wrapper.eq("id", id).last("limit 1");
        // this.getOne(wrapper);
        QueryWrapper<T> wrapper = new QueryWrapper<>();
        wrapper = ReflectUtil.invoke(wrapper, "eq", "parent_id", id);
        wrapper = ReflectUtil.invoke(wrapper, "last", Constants.LIMIT_ONE);
        T one = ReflectUtil.invoke(serviceImpl, "getOne", wrapper);
        return ObjUtil.isNotNull(one);
    }
}

【同样给出使用的例子】

	@Override
    public ResponseResult add(Menu menu) {
        return ResponseResult.okResult(treeService.addTreeNode(menu, Menu.class, this));
    }

    @Override
    @Transactional
    public ResponseResult delete(Integer[] ids) {
        for (Integer id : ids) {
            if (BooleanUtil.isFalse(treeService.removeTreeNode(getById(id), Menu.class, this))) {
                return ResponseResult.errorResult(666, "存在关联数据");
            }
        }
        return ResponseResult.okResult(true);
    }


    @Override
    public ResponseResult edit(Menu menu) {
        return ResponseResult.okResult(treeService.updateTreeNode(menu, Menu.class, this));
    }
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
很抱歉,我无法提供完整的代码实现,因为这需要考虑许多因素,例如数据源、数据结构、业务逻辑等。不同的应用场景可能需要不同的实现方。但是,我可以为您提供一些基本的思路和建议,帮助您实现通用的增删改查模块。 1. Freemarker是一种模板引擎,可以将数据填充到预先定义好的模板中,生成最终的输出。因此,您需要先定义好模板,包括表单、列表、详情等页面的模板。 2. 在Java中,您可以使用Spring框架来实现通用的增删改查功能。Spring提供了许多实用的组件和技术,如JdbcTemplate、MyBatis、Hibernate等,可以方便地访问数据库和处理数据。 3. 针对不同的实体对象,您可以编写通用的Controller和Service类,实现基本的增删改查操作。其中,Service类负责处理业务逻辑,Controller类负责处理请求和响应。 4. 在Controller中,您需要定义好各个请求的路由和参数,如查询参数、分页参数等。同时,还需要处理各种异常情况,如数据格错误、权限问题等。 5. 在Service中,您需要定义好各种业务逻辑,如数据验证、数据转换、事务处理等。同时,还需要处理各种异常情况,如数据不存在、数据冲突等。 6. 针对不同的实体对象,您需要编写对应的DAO接口和实现类,实现数据访问和数据操作。其中,DAO接口定义了基本的增删改查操作,实现类则负责具体的数据库访问和操作。 7. 在DAO实现类中,您可以使用JdbcTemplate、MyBatis、Hibernate等技术访问数据库。同时,还需要处理各种异常情况,如SQL语句错误、数据库连接失败等。 综上所述,实现通用的增删改查模块需要考虑许多因素,包括模板设计、路由设计、参数设计、业务逻辑设计、异常处理等。如果您遇到具体问题,可以在社区中提问,我们会尽力帮助您解决
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奥库甘道夫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值