AJ 组件库之通用数据字典 DataDict

本文介绍了一种数据字典的设计方案,包括表结构设计、JavaBean定义、树状结构的存储方法及其转换为JSON树的过程。此外还提供了获取节点深度的函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求分析

数据字典(Data Dictionary)是一种用户可以访问的记录数据库和应用程序元数据的目录。数据字典在系统也是必备的组件,对此我们想想要怎么做:

  • 基本功能:Key/Value 方式的存储结构,外加其他属性如”说明“,尽可能设计为通用一点
  • 树状结构,也就是说带分类功能的。换句话说,这个数据字典也是一个分类表(数据库层面就是加一个 parentId 字段,当然其他 逻辑较为复杂,而且也不是 pId 这唯一一种树的存储方法)
  • 可以租户/门户隔离,也就是多个门户或者租户。

基本设计

表设计如下。

CREATE TABLE `sys_datadict` (
	`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键 id,自增',
	`name` VARCHAR(20) NULL DEFAULT NULL COMMENT '名称、自定义编码、相当于 key。可选的' COLLATE 'utf8mb4_bin',
	`value` VARCHAR(50) NOT NULL COMMENT '值' COLLATE 'utf8mb4_bin',
	`content` VARCHAR(200) NULL DEFAULT NULL COMMENT '简介、描述' COLLATE 'utf8mb4_bin',
	`parentId` INT(11) NULL DEFAULT NULL COMMENT '父 id',
	`type` INT(11) NULL DEFAULT NULL COMMENT '类型 id',
	`sortNo` TINYINT(4) NULL DEFAULT NULL COMMENT '顺序、序号',
	`stat` TINYINT(4) NULL DEFAULT NULL COMMENT '数据字典:状态',
	`uid` BIGINT(20) NULL DEFAULT NULL COMMENT '唯一 id,通过 uuid 生成不重复 id',
	`tenantId` INT(11) NOT NULL DEFAULT '0' COMMENT '租户 id。0 = 不设租户',
	`createByUser` INT(11) NULL DEFAULT NULL COMMENT '创建者 id',
	`createDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`updateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
	PRIMARY KEY (`id`) USING BTREE,
	INDEX `tenantId` (`tenantId`) USING BTREE
)
COMMENT='数据字典'
COLLATE='utf8mb4_bin';

Java Bean:

import java.util.Date;

import com.ajaxjs.framework.IBaseModel;

/**
 * 数据字典 
 * 
 * @author Frank Cheung<sp42@qq.com>
 * @date 2021-11-07
 */
public class DataDict implements IBaseModel {
    /**
     * 主键 id,自增
     */
    private Long id;

    /**
     * 名称、自定义编码、相当于 key。可选的
     */
    private String name;

    /**
     * 值
     */
    private String value;

    /**
     * 简介、描述
     */
    private String desc;

    /**
     * 父 id
     */
    private Long parentId;

    /**
     * 类型 id
     */
    private Long type;

    /**
     * 顺序、序号
     */
    private Integer sortNo;

    /**
     * 数据字典:状态
     */
    private Integer stat;

    /**
     * 唯一 id,通过 uuid 生成不重复 id
     */
    private Long uid;

    /**
     * 租户 id。0 = 不设租户
     */
    private Long tenantId;

    /**
     * 创建者 id
     */
    private Long createByUser;

    /**
     * 创建时间
     */
    private Date createDate;

    /**
     * 修改时间
     */
    private Date updateDate;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public Long getType() {
        return type;
    }

    public void setType(Long type) {
        this.type = type;
    }

    public Integer getSortNo() {
        return sortNo;
    }

    public void setSortNo(Integer sortNo) {
        this.sortNo = sortNo;
    }

    public Integer getStat() {
        return stat;
    }

    public void setStat(Integer stat) {
        this.stat = stat;
    }

    public Long getUid() {
        return uid;
    }

    public void setUid(Long uid) {
        this.uid = uid;
    }

    public Long getTenantId() {
        return tenantId;
    }

    public void setTenantId(Long tenantId) {
        this.tenantId = tenantId;
    }

    public Long getCreateByUser() {
        return createByUser;
    }

    public void setCreateByUser(Long createByUser) {
        this.createByUser = createByUser;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Date getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }
}

界面:

在这里插入图片描述
在这里插入图片描述

UI 设计也是大头问题,我们后面再讲。

实现难点

树状结构存储

如何在关系型数据库里面存储树状结构一直是需要思考的问题,尤其对于数据结构掌握不好的同学来说,更是难点。这个知识点我前期探索过,参考博文《SQL 双亲节点查找所有子节点》。现在回过头来看,结合大家的一些经验,决定这样子做。

  • MySQL 还是老老实实用 parentId 的存储方法
  • MySQL 8.0 虽然支持 CTE 递归,但老版本就无能为力了。对此,比较简单省力的方式是,直接上 MySQL 函数。

MySQL 函数为 getAllChildren,源码如下:

CREATE DEFINER=`root`@`%` FUNCTION `getAllChildren`(
	`rootId` INT,
	`tableName` VARCHAR(50)
)
RETURNS varchar(4000) CHARSET latin1

BEGIN
	DECLARE P_TEMP VARCHAR(1000);
	DECLARE C_TEMP VARCHAR(1000);
	
	SET P_TEMP = '0';
	SET C_TEMP = CAST(rootId AS CHAR);
	
	WHILE C_TEMP IS NOT NULL DO
		SET P_TEMP = CONCAT(P_TEMP, ',', C_TEMP);
		
		IF tableName = 'sys_datadict' THEN
			SELECT GROUP_CONCAT(id) INTO C_TEMP FROM sys_datadict WHERE FIND_IN_SET(parentId, C_TEMP) > 0; 
		ELSEIF tableName = 'sys_datadict1' THEN
			SELECT GROUP_CONCAT(id) INTO C_TEMP FROM sys_datadict WHERE FIND_IN_SET(parentId, C_TEMP) > 0; 
		ELSE 
			SET C_TEMP = NULL;
		END IF; -- if结束
	END WHILE; 
	 
	RETURN P_TEMP; 
END

调用例子:

SELECT * FROM sys_datadict WHERE FIND_IN_SET(id, getAllChildren(2, 'sys_datadict')) ORDER BY parentId ASC

关键原理是 MySQL 的 find_in_set 函数,顺带学习一下。当然了——换了别的数据库就不是这样子做,通通要改。

getAllChildren() 第一个参数为 INT,是父亲 id,第二个参数是表名,——为什么有表名呢?因为 MySQL 函数不支持动态传入表名作为参数,于是只能写死,因为当初我想这个函数支持多张表而不是一张表写一个函数的。不过我采取代价相对较低的”写死“,在函数里面写死表名,通过 IF ELSE 判断,便可达到支持多张表的目的。

调用上述 SQL 成功返回如下。
在这里插入图片描述

获取节点深度

传入节点 id,返回其深度(depth)。深度即表示节点在树中位于第几层次。

需要一个存储过程和函数来完成。如果你要拿去用,可能要改下 表名、字段名或者出参/入参的类型。

CREATE DEFINER=`root`@`%` PROCEDURE `countLayerHelper`(IN id INT, INOUT counter INT)
BEGIN
	IF EXISTS (SELECT 1 FROM sys_datadict o WHERE o.id = id) THEN
	BEGIN
		DECLARE pid INT DEFAULT 0;
		DECLARE done BOOLEAN DEFAULT FALSE;
		DECLARE _cursor CURSOR FOR SELECT o.parentId FROM sys_datadict o WHERE o.id = id;
		DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
        
		SET counter = counter + 1;
		OPEN  _cursor;
        
		read_loop: LOOP
			FETCH _cursor INTO pid;
			IF done THEN LEAVE read_loop;
			END IF;
			CALL countLayerHelper(pid, counter);
		END LOOP;
        
		CLOSE _cursor;
	END;
	END IF;
END

另外需要一个函数来调用存储过程。

CREATE DEFINER=`root`@`%` FUNCTION `countLayer`(id INT) RETURNS int(11)
BEGIN
	DECLARE counter INT DEFAULT 0;
	SET max_sp_recursion_depth = 255;
	CALL countLayerHelper(id, counter);
	RETURN counter - 1;
END

用户调用:

SELECT countLayer(3) AS depth

参考出处

树节点数组如何转换为 JSON 树?

数据库返回的是一维数组,如何转换为带层次的 JSON 树呢?首先声明一个树节点的类型。

/**
 * 树节点
 */
type TreeMap = {
    id: number;

    /**
     * 父 id,输入时候必填
     */
    parentId: number;

    /**
     * 子节点,生成 Tree 之后出现
     */
    children?: TreeMap[];
}

输入(JSON 起码要有 idparentId 字段):

{"result":[{"stat":null,"updateDate":"2022-03-28 18:12:55","type":null,"content":null,"parentId":9,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":14,"value":"广州新闻","createByUser":null,"createDate":"2022-03-28 18:12:26"}, {"stat":null,"updateDate":"2022-02-26 21:38:28","type":null,"content":null,"parentId":5,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":6,"value":"香港新闻","createByUser":null,"createDate":"2022-02-26 21:38:28"}, {"stat":null,"updateDate":"2022-03-28 15:20:43","type":null,"content":null,"parentId":5,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":7,"value":"澳门新闻","createByUser":null,"createDate":"2022-03-28 15:20:43"}, {"stat":null,"updateDate":"2022-03-28 15:20:55","type":null,"content":null,"parentId":5,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":8,"value":"台湾新闻","createByUser":null,"createDate":"2022-03-28 15:20:55"}, {"stat":null,"updateDate":"2022-03-28 15:22:06","type":null,"content":null,"parentId":4,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":12,"value":"美国新闻","createByUser":null,"createDate":"2022-03-28 15:22:06"}, {"stat":null,"updateDate":"2022-03-28 15:22:14","type":null,"content":null,"parentId":4,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":13,"value":"俄罗斯新闻","createByUser":null,"createDate":"2022-03-28 15:22:14"}, {"stat":null,"updateDate":"2022-03-28 15:21:08","type":null,"content":null,"parentId":3,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":9,"value":"广东新闻","createByUser":null,"createDate":"2022-03-28 15:21:05"}, {"stat":null,"updateDate":"2022-03-28 15:21:23","type":null,"content":null,"parentId":3,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":10,"value":"湖南新闻","createByUser":null,"createDate":"2022-03-28 15:21:23"}, {"stat":null,"updateDate":"2021-11-06 23:58:11","type":null,"content":null,"parentId":2,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":3,"value":"国内新闻","createByUser":null,"createDate":"2021-11-06 23:58:11"}, {"stat":null,"updateDate":"2021-11-06 23:58:25","type":null,"content":null,"parentId":2,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":4,"value":"国际新闻","createByUser":null,"createDate":"2021-11-06 23:58:25"}, {"stat":null,"updateDate":"2021-11-06 23:58:52","type":null,"content":null,"parentId":2,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":5,"value":"港澳台新闻","createByUser":null,"createDate":"2021-11-06 23:58:52"}, {"stat":null,"updateDate":"2021-11-06 23:57:25","type":null,"content":null,"parentId":1,"sortNo":null,"uid":null,"name":null,"tenantId":0,"id":2,"value":"新闻","createByUser":null,"createDate":"2021-11-06 23:57:25"}]}

转换为(多了 children,表示下一级的节点集合):

[{
	"stat": null,
	"updateDate": "2021-11-06 23:58:11",
	"type": null,
	"content": null,
	"parentId": 2,
	"sortNo": null,
	"uid": null,
	"name": null,
	"tenantId": 0,
	"id": 3,
	"value": "国内新闻",
	"createByUser": null,
	"createDate": "2021-11-06 23:58:11",
	"children": [{
		"stat": null,
		"updateDate": "2022-03-28 15:21:08",
		"type": null,
		"content": null,
		"parentId": 3,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 9,
		"value": "广东新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:21:05",
		"children": [{
			"stat": null,
			"updateDate": "2022-03-28 18:12:55",
			"type": null,
			"content": null,
			"parentId": 9,
			"sortNo": null,
			"uid": null,
			"name": null,
			"tenantId": 0,
			"id": 14,
			"value": "广州新闻",
			"createByUser": null,
			"createDate": "2022-03-28 18:12:26",
			"children": []
		}]
	}, {
		"stat": null,
		"updateDate": "2022-03-28 15:21:23",
		"type": null,
		"content": null,
		"parentId": 3,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 10,
		"value": "湖南新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:21:23",
		"children": []
	}]
}, {
	"stat": null,
	"updateDate": "2021-11-06 23:58:25",
	"type": null,
	"content": null,
	"parentId": 2,
	"sortNo": null,
	"uid": null,
	"name": null,
	"tenantId": 0,
	"id": 4,
	"value": "国际新闻",
	"createByUser": null,
	"createDate": "2021-11-06 23:58:25",
	"children": [{
		"stat": null,
		"updateDate": "2022-03-28 15:22:06",
		"type": null,
		"content": null,
		"parentId": 4,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 12,
		"value": "美国新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:22:06",
		"children": []
	}, {
		"stat": null,
		"updateDate": "2022-03-28 15:22:14",
		"type": null,
		"content": null,
		"parentId": 4,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 13,
		"value": "俄罗斯新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:22:14",
		"children": []
	}]
}, {
	"stat": null,
	"updateDate": "2021-11-06 23:58:52",
	"type": null,
	"content": null,
	"parentId": 2,
	"sortNo": null,
	"uid": null,
	"name": null,
	"tenantId": 0,
	"id": 5,
	"value": "港澳台新闻",
	"createByUser": null,
	"createDate": "2021-11-06 23:58:52",
	"children": [{
		"stat": null,
		"updateDate": "2022-02-26 21:38:28",
		"type": null,
		"content": null,
		"parentId": 5,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 6,
		"value": "香港新闻",
		"createByUser": null,
		"createDate": "2022-02-26 21:38:28",
		"children": []
	}, {
		"stat": null,
		"updateDate": "2022-03-28 15:20:43",
		"type": null,
		"content": null,
		"parentId": 5,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 7,
		"value": "澳门新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:20:43",
		"children": []
	}, {
		"stat": null,
		"updateDate": "2022-03-28 15:20:55",
		"type": null,
		"content": null,
		"parentId": 5,
		"sortNo": null,
		"uid": null,
		"name": null,
		"tenantId": 0,
		"id": 8,
		"value": "台湾新闻",
		"createByUser": null,
		"createDate": "2022-03-28 15:20:55",
		"children": []
	}]
}]

使用递归是一个方法,而且异常简洁、直观:

/**
 * 数组转成树
 * 
 * @param list     数组
 * @param parentId 顶级父 id
 * @returns 
 */
function tranListToTreeData(list: TreeMap[], parentId: number): TreeMap[] {
    // 先找到所有的根节点
    let tranList: TreeMap[] = list.filter((it: TreeMap) => it.parentId === parentId);

    // 传入 id  list进行递归 在筛选出 他的父级 是一个数组
    tranList.forEach((itm: TreeMap) => itm.children = tranListToTreeData(list, itm.id));

    return tranList;
}

另外一个方法是这样的,建立一个 Map<id, TreeMap>,然后相当于建立“链表”连接起来,——这个思路也异常简单,而且比递归的方法少了出栈入栈的消耗,效率更高。

/**
 * 数组转成树
 * 
 * @param list 数组
 * @returns JSON Tree
 */
function tranListToTreeData(list: TreeMap[]): TreeMap[] {
    let treeList: TreeMap[] = [];// 最终要产出的树状数据的数组
    let map = {};     // 所有项都使用对象存储起来

    list.forEach((item: TreeMap) => { // 建立一个映射关系:通过 id 快速找到对应的元素
        if (!item.children)
            item.children = [];

        map[item.id] = item;
    });

    list.forEach((item: TreeMap) => {
        // 对于每一个元素来说,先找它的上级
        //    如果能找到,说明它有上级,则要把它添加到上级的 children 中去
        //    如果找不到,说明它没有上级,直接添加到 treeList
        let parent = map[item.parentId];


        if (parent)// 如果存在则表示 item 不是最顶层的数据
            parent.children.push(item);
        else
            treeList.push(item);// 如果不存在 则是顶层数据
    });

    return treeList;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

sp42a

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

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

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

打赏作者

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

抵扣说明:

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

余额充值