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中台项目

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

3528

被折叠的 条评论
为什么被折叠?



