数据库中树形结构表的统一管理,ID-ParentID模式的表的管理

本文介绍了如何管理和操作数据库中采用ID-ParentID模式表示的树形结构表,提出了TreeManager接口和AbstractTreeManager抽象类来统一处理增删改查操作,以及为前端生成树形结构的JSON数据。此外,还详细阐述了Treeable组件、IdPidEntry工具类和IdPidEntryTreeBuilder的使用,以实现对树形结构数据的有效管理。

Github地址

来自 Java中台项目

Tree-Like表

在数据库设计中,通常使用ID-ParentID这样的模式表示上下级关系,最常见的就是category表的设计,比如

CREATE TABLE IF NOT EXISTS `item`.`item_category` (
  `item_category_id` BIGINT UNSIGNED NOT NULL,
  `category_name` VARCHAR(45) NOT NULL COMMENT '分类名称',
  `parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父级分类ID'
  PRIMARY KEY (`item_category_id`)
)
COMMENT = '商品分类'

使用parent_id表示上级,这种表示上下级的方式在数据库设计中很通用,而且在一个系统中,通常有多个表都是这样的结构,比如文章分类表,部门,组织机构等等。这种类型的表我们可以认为是tree-like的表,因为可以表示树形结构。

相同的树形结构操作

每个表自身有单独的业务逻辑,除了这些独有的业务逻辑外,对于树形结构方面,他们有很多操作是相同的,比如

  • 新增,更新时,同一个父节点下不能有同名的节点
  • 删除时,如果下级还有子节点,则不能被删除
  • 获取某个节点的children,祖先,后裔列表
  • 生成树形结构
  • 为前端生成方便展示成树形结构的JSON
  • 改变节点的父级时,维护树的完整性,比如节点的depth(level)的维护
  • 等等

既然在树形结构方面有这么多相同的操作,因此,是否可以统一管理tree-like表在树形结构方面的操作呢?答案是可以,以下我们从几个方面给出解决方案

TreeManager接口

TreeManager接口用于定义tree-like相关表的增,删,改,查操作,接口定义如下(内容太多,因此只列出了创建节点的方法的定义)

package com.wuda.foundation.commons;

import com.wuda.foundation.lang.AlreadyExistsException;
import com.wuda.foundation.lang.CreateMode;
import com.wuda.foundation.lang.CreateResult;
import com.wuda.foundation.lang.RelatedDataExists;

import java.util.List;

/**
 * 树形结构管理.
 * 很多术语参考<a href="http://www.cs.columbia.edu/~allen/S14/NOTES/trees.pdf">trees</a>
 *
 * @param <C> 用于创建节点的参数的类型
 * @param <U> 用于更新节点的参数的类型
 * @param <D> 用于描述节点的类型
 * @author wuda
 * @since 1.0.3
 */
public interface TreeManager<C extends CreateTreeNode, U extends UpdateTreeNode, D extends DescribeTreeNode> {

    /**
     * 创建一个节点.
     *
     * @param createTreeNode 新增节点的参数
     * @param createMode     create mode
     * @param opUserId       操作者用户ID
     * @return 创建结果
     * @throws AlreadyExistsException       比如节点名称在同一个父节点下已经存在(同一个父节点下不允许存在同名的子节点)
     * @throws ParentNodeNotExistsException 父节点不存在
     */
    CreateResult createTreeNode(C createTreeNode, CreateMode createMode, Long opUserId) throws AlreadyExistsException, ParentNodeNotExistsException;

    /**
     * 在给定的父节点下是否已经存在指定名称的节点.
     *
     * @param parentId  parent id
     * @param childName 子节点名称
     * @return <code>true</code>-如果存在
     */
    boolean checkNameExists(Long parentId, String childName);

}

AbstractTreeManager

抽象实现类用于完成共有的逻辑(类似JDBC模板代码的实现),定义如下

package com.wuda.foundation.commons;

import com.wuda.foundation.lang.AlreadyExistsException;
import com.wuda.foundation.lang.Constant;
import com.wuda.foundation.lang.CreateMode;
import com.wuda.foundation.lang.CreateResult;
import com.wuda.foundation.lang.RelatedDataExists;
import com.wuda.foundation.lang.tree.IdPidEntryUtils;
import org.apache.commons.lang3.StringUtils;

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

/**
 * 通用树形管理的抽象实现类.
 *
 * @param <C> 用于创建节点的参数的类型
 * @param <U> 用于更新节点的参数的类型
 * @param <D> 用于描述节点的类型
 * @author wuda
 * @since 1.0.3
 */
public abstract class AbstractTreeManager<C extends CreateTreeNode, U extends UpdateTreeNode, D extends DescribeTreeNode> implements TreeManager<C, U, D> {

    @Override
    public CreateResult createTreeNode(C createTreeNode, CreateMode createMode, Long opUserId) throws AlreadyExistsException, ParentNodeNotExistsException {
        boolean isCreatingRootTreeNode = isCreatingRootTreeNode(createTreeNode);
        DescribeTreeNode parentTreeNode = null;
        if (!isCreatingRootTreeNode) {
            parentTreeNode = getTreeNode(createTreeNode.parentId);
            if (parentTreeNode == null) {
                throw new ParentNodeNotExistsException("parent tree node id = " + createTreeNode.parentId + ",不存在");
            }
        }
        boolean nameExists = checkNameExists(createTreeNode.parentId, createTreeNode.name);
        if (nameExists) {
            throw new AlreadyExistsException("parent tree node id = " + createTreeNode.parentId + ",已经存在名称为 " + createTreeNode.name + " 的子节点");
        }
        supplementArg(createTreeNode, isCreatingRootTreeNode, parentTreeNode);
        CreateResult createResult = createTreeNodeDbOp(createTreeNode, createMode, opUserId);
        if (createResult.getExistsRecordId() != null) {
            throw new AlreadyExistsException("树中已经存在");
        }
        return createResult;
    }

    /**
     * 为正在创建的节点补充参数,比如补充root tree node id.
     *
     * @param creating               正在创建的节点
     * @param isCreatingRootTreeNode 正在创建的节点是否root node
     * @param parentTreeNode         正在创建的节点的父节点
     */
    private void supplementArg(CreateTreeNode creating, boolean isCreatingRootTreeNode, DescribeTreeNode parentTreeNode) {
        long rootTreeNodeId;
        int depth;
        if (!isCreatingRootTreeNode) {
            if (isRootTreeNode(parentTreeNode)) {
                rootTreeNodeId = parentTreeNode.getId();
            } else {
                rootTreeNodeId = parentTreeNode.getRootId();
            }
            depth = parentTreeNode.getDepth() + 1;
        } else {
            rootTreeNodeId = Constant.NOT_EXISTS_ID;
            depth = 1;
        }
        creating.setRootId(rootTreeNodeId);
        creating.setDepth(depth);
    }

    /**
     * 新增节点实际执行的数据库操作.
     *
     * @param createTreeNode 创建节点的参数
     * @param createMode     create mode
     * @param opUserId       操作人用户ID
     * @return 新建的节点的ID
     */
    protected abstract CreateResult createTreeNodeDbOp(C createTreeNode, CreateMode createMode, Long opUserId);

}

以创建节点(比如在item_category表新增一条记录,就表示创建一个节点)为例

  • isCreatingRootTreeNode,检查是否正在创建根节点,如果不是,则父节点必须存在
  • checkNameExists,检查同一个父节点下是否已经存在给定名称的节点
  • supplementArg,用于补充rootId,depth(level)等其他树形结构相关的属性
  • 最后,createTreeNodeDbOp方法即是每个tree-like表应该实现的逻辑,比如对于item_category表,即实现insert into item_category表

对于文章分类,部门,组织结构等表的树形结构的维护,只需要实现AbstractTreeManager抽象类,然后实现对具体表的简单的增,删,改,查方法即可,比如item_category表的实现类伪代码如下

public class ItemCategoryManagerImpl extends AbstractTreeManager {

    @Override
    protected CreateResult createTreeNodeDbOp(CreateItemCategory createItemCategory, CreateMode createMode, Long opUserId) {
        //实现 insert into item_category表
        return result;
    }
} 

为前端输出可展示成树形结构的JSON

每个公司的前端要求的格式可能不一样,这里我们提供一种可能的格式

Treeable组件

Treeable组件定义如下

package com.wuda.foundation.lang.tree;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 可以展示树形结构的类.该类主要的用法就是方便把树形结构生成Json,
 * 供Html等前端程序展示树形结构,理解这个类最直接的方式就是使用jackson等Json库输出Json,
 * 查看该Json的格式就可以很好理解了.比如
 * <pre>
 * <block>
 * {
 * 	"node": {
 * 		"name": "湖南省",
 * 		"id": 2,
 * 		"pid": 1
 *        },
 * 	"children": [{
 * 		"node": {
 * 			"name": "张家界市",
 * 			"id": 3,
 * 			"pid": 2
 *        },
 * 		"children": [{
 * 			"node": {
 * 				"name": "桑植县",
 * 				"id": 5,
 * 				"pid": 3
 *            },
 * 			"children": null
 *        }]
 *    }, {
 * 		"node": {
 * 			"name": "长沙市",
 * 			"id": 4,
 * 			"pid": 2
 *        },
 * 		"children": null
 *    }]
 * }
 * </block>
 * </pre>
 *
 * @author wuda
 * @since 1.0.3
 */
public class Treeable<T extends Comparable<T>, N extends IdPidEntry<T>> implements Serializable {
    /**
     * 当前节点.
     */
    private N node;
    /**
     * 当前节点的下级.
     */
    private List<Treeable<T, N>> children;

    /**
     * set node.
     *
     * @param node node
     */
    public void setNode(N node) {
        this.node = node;
    }

    /**
     * get node.
     *
     * @return node
     */
    public N getNode() {
        return node;
    }

    /**
     * add child.
     *
     * @param child child
     */
    public void addChild(Treeable<T, N> child) {
        if (this.children == null) {
            children = new ArrayList<>();
        }
        children.add(child);
    }

    /**
     * get children.
     *
     * @return children
     */
    public List<Treeable<T, N>> getChildren() {
        return children;
    }
}

对于tree-like表的记录,我们可以方便的生成Treeable实体

实用的工具类

IdPidEntry,IdPidEntryUtils

我们能够统一的管理树形结构,最核心的特点就是:数据具有id-parentId的特点,因此我们把这类数据抽象为IdPidEntry,然后提供了相关的工具类IdPidEntryUtils

Tree

不用多说了,就是树形结构

IdPidEntryTreeBuilder

给定扁平的IdPidEntry集合,我们就可以通过IdPidEntryTreeBuilder这个类生成Tree结构,因为数据之间具有id-parentId relationship,因此,肯定可以生成Tree

Github地址

来自 Java中台项目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值