聊一下“数据字典”
数据字典的含义非常广泛, 通俗地讲, 是指"关于数据项的数据", 一个项目中, 所有数据的来源, 去向, 存储等各个环节, 对数据的说明都可以是"数据字典"的范畴.
我在这里想讲的"数据字典",是狭义上, 特指数据编码,举个栗子,就是如性别这个数据项,在数据库和代码中,用 '1' 表示男性, '2' 表示女性。
也有人叫这是数据字典,名字高大上一些。
不好的栗子
也许有人认为这个很简单,但其实,我碰到过很多人都不能恰当使用它,从而导致程序性能上的低下或代码隐患,前人编程后人掉坑的情况。
类似以下的,我认为都是不好的栗子:
如:
表 user(id, name, gender, ....)
表 dict_gender(id, value, label, ...)
在mybatis 的xml 文件sql语句中,
select a.name, b.label from user a
left join dict_gender b on a.gender = b.value;
更有甚者
select gender_label, count(*) from
(
select a.id, b.label as gender_label as from user a left
join dict_gender b on a.gender = b.value
);
诸如之类的代码。 它们将 数据值与需要在用户视图界面的显示标签的转换,放在数据库层面来处理,一个user表有几个编码字段,就left join 几个码表,导致程序的效率低下,性能极低,如果你在写这样的代码,建议阅读此文。如你是大牛,也帮我看看,提个意见,不胜感激!
还有就是在java代码中, 类似 if (myUser.getGender().equals("1")){...} 这样的代码,
"1", "2" 指的分别是什么呢? 给阅读者造成不必要的困绕。
我的解决方法
我认为,界面显示层的问题就应该直接交给前端浏览器处理,后端给前端的数据直接用原值。
后端代码,应该用有意义常量或者枚举值对"1", "2"这样的编码作一个封装。
下面是我的解决方法,分享出来,
建表语句
CREATE TABLE `mandy_dict` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '程序中用的名字,数据字段注释:后的名字',
`label` varchar(255) NOT NULL COMMENT '显示用的名字',
`mandy_const` varchar(255) DEFAULT NULL COMMENT '常量类名',
`remark` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='字典表';
CREATE TABLE `mandy_dict_item` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`dict_id` bigint(20) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL COMMENT '常量名称',
`value` varchar(255) DEFAULT NULL COMMENT '数据表中存放的值',
`label` varchar(255) DEFAULT NULL COMMENT '前端显示的值',
`remark` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COMMENT='字典明细表';
数据样例
mandy_dict
1 gender 性别 Gender 性别
2 security_level 安全级别 SecurityLevel 安全级别参数配置
mandy_dict_item
1 1 MALE 1 男 性别-男
2 1 FEMALE 2 女 性别-女
3 2 TOP_SECRET 1 绝密级 TOP_SECRET:绝密级
4 2 SECRET 2 机密级 SECRET:机密级
5 2 CONFIDENTIAL 3 秘密级 CONFIDENTIAL:秘密级
6 2 UNCLASSIFIED 4 普通级 UNCLASSIFIED:普通级
7 2 PUBLIC 5 已公开 PUBLIC:已公开
生成Java代码的SQL语句
定义好所有的字典数据项后,执行如下语句
select * from
(
select -1 as line_nbr, 'package cn.mandy.sysadmin.constant;\n\npublic interface MandyConstant {' as content
union all
select id * 100000 as line_nbr, concat('/**\n* name:', name, '\n* label:', '\n* remark:', remark, '\n*/\n final class ', mandy_const, '{') as content from mandy_dict
union all
select a.id * 100000 + b.id as line_nbr, concat(' public static final String ', b.name, ' = "', b.value, '"; // label:', b.label, ' remark:', b.remark) as content from mandy_dict a, mandy_dict_item b
where a.id = b.dict_id
union all
select id * 100000 + 10000 as line_nbr, concat('} \n') as content from mandy_dict
union all
select 999999 as line_nbr, '}' as content
) all_line order by line_nbr,
生成的JAVA代码
将输出直接拷贝到一个程序文件中,是一个接口。 用SQL自动生成代码,既可提高效率,也可以避免手写出错,提高正确率,给项目管理也带来好处。
package cn.mandy.sysadmin.constant;
public interface MandyConstant {
/**
* name:security_level
* label:
* remark:安全级别参数配置
*/
final class SecurityLevel{
public static final String TOP_SECRET = "1"; // label:绝密级 remark:TOP_SECRET:绝密级
public static final String SECRET = "2"; // label:机密级 remark:SECRET:机密级
public static final String CONFIDENTIAL = "3"; // label:秘密级 remark:CONFIDENTIAL:秘密级
public static final String UNCLASSIFIED = "4"; // label:普通级 remark:UNCLASSIFIED:普通级
public static final String PUBLIC = "5"; // label:已公开 remark:PUBLIC:已公开
}
/**
* name:gender
* label:
* remark:性别
*/
final class Gender{
public static final String MALE = "1"; // label:男 remark:性别-男
public static final String FEMALE = "2"; // label:女 remark:性别-女
}
}
小小的说明一下
然后在程序中,就可以像这样愉快地使用了。
if (MandyConstant.Gender.MALE.equals(myUser.getGender()){...}
(小技巧: 用 MandyConstant.Gender.MALE.equals(myUser.getGender()) 而不是 myUser.getGender().equals(MandyConstant.Gender.MALE) 可以避免NPE )
用MALE而不是 "1", 程序的可读性是不是大大增强了,而且,想要查看程序中关于这个数据项的处理逻辑,还可以用 MandyConstant.Gender 或者 MandyConstant.Gender.MALE
来进行查找,对读源码查BUG带来极大的便利。
在mandy_dict表中, mandy_const的值对应着类名, 若 mandy_const 为空, 则不生成常量类。通常来说,会对程序流程产生影响,需要写成常量的数据项(如各种状态值),
它们的枚举值数据一般不会太多,而枚举值多的数据项(如民族,行政区域等)通常不影响程序流程。用 mandy_const 空或非空就可以将这两类数据进行区分。
以上是后端的处理方式。
前端的处理
前端界面展示, 通常只是作一个 value 到 label 的转换,还有用到 select, list 等的时候,需要用到所有的枚举条目。
可以如下处理(以下是vue中的 js代码, 不复杂,相信聪明如您一定看得懂)。
//关键sql
// select d.name as dict_key, di.value as dict_value, di.label as dict_label from mandy_dict d, mandy_dict_item di
// where di.dict_id = d.id
// list 是从后端执行以上sql返回的数据,sysDict 存放的是一个 map(mandy_dict.name, map(mandy_dict_item.value, mandy_dict_item.dict_label))
SYS_DICT_INIT: (stat, list) => {
for (var i = 0, len = list.length; i < len; i++) {
var dest = null;
if (!stat.sysDict.has(list[i].dictKey)) {
dest = new Map();
stat.sysDict.set(list[i].dictKey, dest);
} else {
dest = stat.sysDict.get(list[i].dictKey);
}
dest.set(list[i].dictValue, list[i].dictLabel);
}
},
import store from "../store";
export function getLabel(dictKey, dictValue) {
let myMap = new Map();
myMap = store.getters.sysDict;
if (!myMap.has(dictKey)) {
return "未定义";
}
let keyMap = new Map();
keyMap = myMap.get(dictKey);
if (!keyMap.has(dictValue)) {
return "未定义";
}
return keyMap.get(dictValue);
}
export function getEntries(dictKey) {
if (!store.getters.sysDict.has(dictKey)) {
return [];
}
let ret = [];
for (var [value, label] of store.getters.sysDict.get(dictKey)) {
let rec = {
value: value,
label: label
};
ret.push(rec);
}
return ret;
}
<template> HTML 段就可以类似这样使用了 {{getDictLabel("gender", row.gender)}}
像这样,不是说把活交给还前端人员,把皮球踢给前端,更大的意义在于,在客户端浏览器上去做这些value, label转换可以极大的降低数据库的压力。
写在后面
数据库做join 类似操作是非常消耗资源的,阿里的java开发手册(1.4.0 索引规约, 第二条)中,明确地提出超过三个表禁止join, 要让join做更有意义的事,join业务数据表,而不是码表。不了解这个开发手册的可以去百度了解一下这个开发规范,这个手册是有非常高价值的.
好,分享到这里,谢谢!