Spring Boot实现高质量的CRUD-1

这篇具有很好参考价值的文章主要介绍了Spring Boot实现高质量的CRUD-1。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、前言

​	在Spring Boot的SMM框架(SpringBoot+Mysql+Mybatis)的WEB项目中,CRUD(增删改查)大致占了50%-70%左右的工作量。提高CRUD的代码质量,提高CRUD的开发效率,是一件值得探讨的事项。
​	一般认为,CRUD是一件体力活。在SMM框架项目开发中,项目团队通常将这类CRUD的开发任务交由知道如何写Mybatis脚本的初中级Java工程师来做,一般往往仅仅是业务层面的交代。实践表明,如果不在一开始制定CRUD的开发规范,得到的结果往往差强人意。由于缺乏整体规划,不同工程师的代码风格迥异,方法命名,参数命名,五花八门,检查项也不完整,代码臃肿,重复性高,后期常常需要大量修修补补,维护成本高得惊人。	​	
​	而基于CRUD开发规范,则可大大提高了CRUD代码的易维护性、接口的可用性和健壮性、用户友好性,以及功能扩展灵活性。多个项目实践显示,使用CRUD开发规范至少可以提高20%的开发效率,考虑包括后期维护成本,可以有效降低开发成本超过30%。

2、CRUD的常规功能

​	CRUD的常规功能如下:
​	1)增加单个对象;    
​	2)批量增加对象;
​	3)修改单个对象;
​	4)批量修改对象;
​	5)删除单个对象;
​	6)批量删除对象;
​	7)前端分页查询;    
​	8)获取指定对象;    
​	9)根据条件查询对象;    
​	10)导入Excel数据;
​	11)导出Excel数据。

​	其中,导入Excel数据是批量增加或修改对象的一种方式,导出Excel数据是数据查询的一种形式。Excel作为最方便的办公文件格式,在数据交换中使用十分广泛。
​	根据代码分层开发思想,CRUD将涉及8个文件:
​	1)实体类Entity,基本与表结构字段进行映射对应;简单起见,可忽略其它POJO对象,在实体类中添加其它需要的属性。
​	2)数据访问对象类Dao,这个在SMM框架中,为支持Mybatis或Mybatis-Plus的接口类。
​	3)Dao实现类Mybatis,为xml脚本文件,使用Mybatis脚本语言实现Dao的相关接口。
​	4)服务接口类Service,定义Controller层接口所需的各种CRUD接口形式,以及内部处理所需的其它接口形式。
​	5)服务接口实现类ServiceImpl,实现Service的相关接口。
​	6)服务单元测试类ServiceTest,使用JUnit,实现Service的相关接口的单元测试。
​	7)业务方法接口类Controller,也可称为API接口类,提供HTTP接口服务。    
​	8)API接口文档,格式比较灵活,当然使用YAPI导出的格式最好,但录入很费时间。当然不管使用哪种格式,API接口相关的要素说明都要有。    

3、数据库设计规范

​	由于CRUD与数据库表结构高度相关​,因此先简单说明一下表结构的设计规范。

3.1、表结构设计例子

​	先看2个表结构的DDL(Data Definition Language,数据库定义语言)脚本例子。
-- ----------------------------
-- Table structure for exa_users
-- 用户表
-- ----------------------------
DROP TABLE IF EXISTS exa_users;
CREATE TABLE exa_users
(
  user_id			BIGINT(20)        NOT NULL DEFAULT 0	COMMENT '用户ID',

  -- 登录信息
  user_name			VARCHAR(80)		UNIQUE NOT NULL DEFAULT '' COMMENT '用户名',
  -- 加密算法:md5(concat(md5(密码明文),salt));
  -- 前端发送一次md5的值到服务器,服务器添加salt值,计算二次md5(32)大写值,与passwd值比较
  password			VARCHAR(64)		NOT NULL DEFAULT '' COMMENT '用户密码',
  -- salt,可用记录生成的时间
  salt				VARCHAR(64)		NOT NULL DEFAULT '' COMMENT '加盐md5算法中的盐',
  user_type			TINYINT(4)		NOT NULL DEFAULT 3  COMMENT '用户类型,1-系统管理员、2-公司内部用户、3-外部用户,由系统参数表user_type类别定义',
  org_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '组织ID',

  -- 用户资料
  real_name			VARCHAR(64)		NOT NULL DEFAULT '' COMMENT '真实姓名',
  email				VARCHAR(100)    	NOT NULL DEFAULT '' COMMENT 'Email',
  phone_number  		VARCHAR(20)		NOT NULL DEFAULT '' COMMENT '手机号码',
  sex				TINYINT(4)		NOT NULL DEFAULT 1  COMMENT '性别,1-无值、2-男、3-女、4-其它,由系统参数表sex类别定义',
  birth				DATETIME		DEFAULT NULL COMMENT '生日',
  id_no				VARCHAR(30)		NOT NULL DEFAULT '' COMMENT '身份证号码',

  open_id			VARCHAR(40)		NOT NULL DEFAULT "" COMMENT '微信小程序的openid',
  woa_openid    		VARCHAR(40)		NOT NULL DEFAULT "" COMMENT '微信公众号openid',
  remark			VARCHAR(200)	NOT NULL DEFAULT '' COMMENT '备注',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,0-正常、1-禁用,由系统参数表delete_flag类别定义',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (user_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT '用户表';

-- ----------------------------
-- Table structure for exa_user_roles
-- 用户和角色关系表
-- 用户和角色是多对多关系
-- ----------------------------
DROP TABLE IF EXISTS exa_user_roles;
CREATE TABLE exa_user_roles
(
  user_id			BIGINT(20)		NOT NULL DEFAULT 0  COMMENT '用户ID',
  role_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '角色ID',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,保留字段',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME 		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (user_id, role_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='用户和角色关系表';

3.2、表结构设计规范

​	1)表名使用项目简称前缀,这个前缀一般为3-4个字符,使用了表名前缀,数据表属于哪个项目一目了然。由于不同项目,有一些如用户表,角色表等通用的表,对于测试团队而言,有了表的前缀,只需一个测试数据库,就可以支持多个项目的测试。表名一般使用复数形式后缀,因为数据表是对象或关系的集合。
​	2)字段名采用下划线规则,小写字母,使用英文词汇或易理解的英文缩写,不能使用汉语拼音或汉语拼音缩写,那样会令人费解。
​	3)字符串类型使用VARCHAR类型,长文本考虑TEXT或更长的TEXT类型。
​	4)枚举类型一般使用TINYINT类型,更长的可考虑INT类型,且枚举类型均在系统参数表定义,不建议使用字符串作为枚举值。
​	5)除DATETIME、BLOB和TEXT类型外,均使用NOT NULL DEFAULT 形式。
​	6)TIMESTAMP类型,为4字节,有至多到2038年问题,为避免类似于千年虫问题,不再使用,使用DATETIME类型。如果需要精确到毫秒,可以用DATETIME(3),NOW默认值对应为NOW(3)。
​	7)所有字段都要有注释,即有COMMENT部分。
​	8)所有表都要有记录操作信息部分,即有operator_name、delete_flag、create_time、update_time这4个字段。operator_name值为空串,表示系统内部生成的记录(如统计记录);delete_flag字段,对于对象表(如用户表),通过delete_flag字段的值,启用或禁用对象,不做记录的物理删除;对于关系表(如用户和角色关系表),则支持物理删除,delete_flag字段保留。create_time和update_time用于标记记录的生成时间和最后修改时间,insert和update时无需考虑,由数据库自动维护,这2个字段在记录的增量使用时,非常有用,如表记录的定时同步,或采样数据的增量统计和分析。
​	9)对象表的主键ID字段,使用全局ID,不建议使用自增ID,因为自增ID在进行数据库的集群或分布式扩容以及数据迁移时,会遇到很大麻烦。
​	10)ID字段名称,应尽量使用符合对象身份的ID名称,如user_id、org_id等,而不应随便都用id或rec_id这样的通用名称(特殊情况除外),这样处理一方面可降低沟通成本,另外在多表联结时,也不必考虑字段别名以及别名与实体类属性的映射,可降低开发成本。另外,ID字段尽量使用INTEGER或BIGINT类型,而避免使用字符串类型,这样多表联结查询有性能优势。
​	11)要有主键字段定义。
​	12)要有表名注释。
​	13)关于数据库引擎,使用InnoDB,目前阿里云数据库已取消了对MYISAM的支持,因为两者性能已差别不大,而MYISAM不支持事务处理,显然不如统一使用InnoDB。
​	14)关于字符集,一般使用utf8,特殊场景,如需支持微信表情,则可考虑utf8mb4。
​	15)密码字段,考虑信息安全,签名密码可使用签名算法如MD5,第三方系统的账号密码如邮箱密码等,使用AES算法存储。
​	16)如果本表涉及数据权限,则应添加相关数据权限字段,以方便数据权限控制,这将在数据权限讨论中详细展开。

​	表结构设计,必须有字段说明,对于Mysql数据库而言,字段和表注释可以直接在建表语句中加入,这样即使不查阅表结构设计文档,也可利用数据库访问工具如Navicat等,通过查看表的DDL脚本了解各字段的含义和用处。对于如Hibernate、Golang等支持ORM(Object-Relation Mapping,对象关系映射)的框架,可以在代码中直接添加字段,这个原则上是不允许的。没有字段描述的表结构DDL脚本,是不可取的,只会给后人留下太多的坑,从而大大提高了维护成本。

3.3、使用系统参数表管理字段枚举值

​	关于系统参数表,表结构及记录的例子如下:
-- ----------------------------
-- Table structure for exa_sys_parameters
-- 系统参数表
-- ----------------------------
DROP TABLE IF EXISTS exa_sys_parameters;
CREATE TABLE exa_sys_parameters
(
  class_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '参数类别ID',
  class_key			VARCHAR(60)		NOT NULL DEFAULT '' COMMENT '参数类别key',
  class_name		VARCHAR(60)		NOT NULL DEFAULT '' COMMENT '参数类别名称',
  item_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '参数类别下子项ID',
  item_key			VARCHAR(200)	NOT NULL DEFAULT '' COMMENT '子项key',
  item_name			VARCHAR(60)		NOT NULL DEFAULT '' COMMENT '子项名称',
  item_value		VARCHAR(200)	NOT NULL DEFAULT '' COMMENT '子项值',
  item_desc			VARCHAR(512)	NOT NULL DEFAULT '' COMMENT '子项描述',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,0-正常、1-已删除',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (class_id, item_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT '系统参数表';

CREATE INDEX exa_sys_parameters_class_key_item_key ON exa_sys_parameters (class_key, item_key);

-- 10001-10099 保留给基础表,即用户、权限相关的类型定义

INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10001, 'user_type', '用户类型', 1, '1', 'admin','系统管理员', '');
INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10001, 'user_type', '用户类型', 2, '2', 'internal person','公司内部用户', '');
INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10001, 'user_type', '用户类型', 3, '3', 'external person','外部用户', '');

INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10002, 'sex', '性别', 1, '1', 'none','无值', '');
INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10002, 'sex', '性别', 2, '2', 'male','男', '');
INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10002, 'sex', '性别', 3, '3', 'female','女', '');
INSERT INTO exa_sys_parameters(class_id, class_key, class_name, item_id, item_key, item_name,item_value, item_desc)
VALUES (10002, 'sex', '性别', 4, '4', 'other','其它', '');
​	关于系统参数表的作用,参见:(<a href="https://www.cnblogs.com/alabo1999/p/14907461.html" target="_blank">使用系统参数表,提升系统的灵活性</a>)。
​	系统参数也可以复用,如true_false类别,0表示否,1表示是,这个系统参数类别可以为多个表的枚举字段使用。
​	系统参数表的item_name可用于Java的枚举类的枚举项的名称,这样可提高代码的可读性,在接口参数校验时,对于枚举字段,需要使用枚举类型进行值的合法性检查。在后面提到的单元测试随机构造测试样本时,枚举字段的取值受此枚举类约束。

3.4、关于全局ID算法

​	全局ID可以有多种生成方法,如基于Redis的算法,雪花算法等,此处提供了一种基于Mysql数据库的全局ID方案,包括2张表和一个函数。
-- ----------------------------
-- Table structure for exa_table_code_config
-- ID编码配置表
-- ----------------------------
DROP TABLE IF EXISTS exa_table_code_config;
CREATE TABLE exa_table_code_config
(
  table_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '表ID',
  table_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '表名称',
  field_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT 'ID字段名称',

  -- 格式化编码使用,如15编码为HR000015,即数字前面用0填补。
  prefix			VARCHAR(20)		NOT NULL DEFAULT '' COMMENT '编码前缀字符串',
  prefix_len		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '编码前缀字符串长度',
  seqno_len			TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '序列号长度',
  PRIMARY KEY (table_id)
) ENGINE = Innodb
  DEFAULT CHARSET = utf8 COMMENT 'ID编码配置表';

CREATE INDEX exa_table_code_config_table_name ON exa_table_code_config(table_name);

-- 本系统的table_id以10001开始
INSERT INTO exa_table_code_config(table_id,table_name,field_name) 
VALUES(10001,'exa_users','user_id');
INSERT INTO exa_table_code_config(table_id,table_name,field_name) 
VALUES(10002,'exa_roles','role_id');
INSERT INTO exa_table_code_config(table_id,table_name,field_name) 
VALUES(10004,'exa_orgnizations','org_id');
INSERT INTO exa_table_code_config(table_id,table_name,field_name) 
VALUES(10005,'exa_functions','func_id');

-- ----------------------------
-- Table structure for exa_table_id_allocate
-- ID最新可用值表
-- ----------------------------
DROP TABLE IF EXISTS exa_table_id_allocate;
CREATE TABLE exa_table_id_allocate
(
  table_id			INT(11)			NOT NULL DEFAULT 0 COMMENT '表ID',
  last_id			BIGINT(20)		NOT NULL DEFAULT 0 COMMENT '最新可用ID值',
  PRIMARY KEY (table_id)
) ENGINE = Innodb
  DEFAULT CHARSET = utf8 COMMENT 'ID最新可用值表';

-- 初始记录
-- exa_users 预留10个账号,保留给内部系统使用
INSERT INTO exa_table_id_allocate(table_id,last_id) VALUES(10001,11);
INSERT INTO exa_table_id_allocate(table_id,last_id) VALUES(10002,4);
INSERT INTO exa_table_id_allocate(table_id,last_id) VALUES(10004,2);
INSERT INTO exa_table_id_allocate(table_id,last_id) VALUES(10005,1000);

-- ----------------------------------------------------------------------
-- 函数:获取全局ID
-- tableid: 表ID
-- record_count: 需要分配ID的记录条数,大于等于1
-- return: 第一条记录的ID
-- ----------------------------------------------------------------------
DELIMITER ;
DROP FUNCTION IF EXISTS exa_get_global_id;
CREATE FUNCTION exa_get_global_id(tableid INT(11), record_count INT(11))
  RETURNS BIGINT(20)
  DETERMINISTIC
BEGIN
  UPDATE exa_table_id_allocate
  SET
    last_id = (@exaid := last_id) + record_count
  WHERE table_id = tableid;
  RETURN @exaid;
END;
​	经过测试,这种方法获取的全局ID约为1000条/秒,而自增ID的获取速度约为600-800条/秒,另外当表的字段越多,自增ID的性能越差,而此方法的性能不受字段数目影响。
​	当有高频记录ID需要处理时,针对这类ID,可由服务器每次批量领取一定数目的ID,如1000个连续ID,服务器内部使用内存分发ID,这样就可以达到100万条/秒的处理性能,有点类似于雪花算法了,这与服务器是否分布式部署无关。
​	注意:@exaid为Mysql的全局变量,因此不同项目需要使用不同的变量名。

3.5、关于字段名下划线到属性字段驼峰的映射

​	Java的实体类属性字段名,一般使用驼峰规则。这样就涉及到下划线到驼峰的映射。
​	首先,配置文件需要设置mybatis使用下划线转驼峰的映射:
mybatis.configuration.map-underscore-to-camel-case=true
​	这样,表字段名可以映射实体类的属性名。
​	另外,需要注意的是,对于返回类型为Map<String, Object>或List<Map<String, Object>>的查询,字段名不会从下划线转为驼峰(参见:<a href="https://www.definesys.com/blog/post/18409.html" target="_blank">mybatis Map查询结果下划线转驼峰的实例</a>)。
​	这里提供另外一种解决思路,即开发下列三个公共方法:
	// 下划线字符串转为驼峰字符串
	public static String underlineToCamel(String input);

	// key为下划线字符串转为key为驼峰字符串的字典
	public static Map<String, Object> underlineToCamel(Map<String, Object> map);

	// key为下划线字符串转为key为驼峰字符串的字典列表
	public static List<Map<String, Object>> underlineToCamel(List<Map<String, Object>> mapList);

​	对于返回类型为Map<String, Object>或List<Map<String, Object>>的查询结果,先调用underlineToCamel方法处理,然后就可以用属性名访问了。

3.6、权限管理

​	权限管理与数据库设计有关。
​	权限管理分两个层面,一个是功能权限,一个是数据权限。

3.6.1、功能权限管理

​	功能权限是用户可以观察和操作的页面元素的权限,包括菜单权限和功能操作权限,这个通常是基于RBAC(Role-Based Access Control),即基于角色的访问控制,就是角色表、功能表、角色与功能关系表、用户与角色关系表,来定义功能项,角色项,每种角色可以操作的功能集合,用户拥有的角色集合,从而得到用户可以操作的功能集合。
​	下面是相关表结构:
-- ----------------------------
-- Table structure for exa_functions
-- 功能表
-- ----------------------------
DROP TABLE IF EXISTS exa_functions;
CREATE TABLE exa_functions
(
  func_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '功能ID',
  func_name			VARCHAR(100)	NOT NULL DEFAULT '' COMMENT '功能名称',
  parent_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '父功能ID',
  level				TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '功能所在层级',
  order_no			INT(11)			NOT NULL DEFAULT 0  COMMENT '显示顺序',
  url				VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '访问接口url',
  dom_key			VARCHAR(80)		NOT NULL DEFAULT '' COMMENT 'dom对象的ID',
  img_tag			VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '节点icon名称',

  remark			VARCHAR(200)	NOT NULL DEFAULT '' COMMENT '备注',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,0-正常、1-已删除',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (func_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='功能表';

-- ----------------------------
-- Table structure for exa_roles
-- 角色表
-- ----------------------------
DROP TABLE IF EXISTS exa_roles;
CREATE TABLE exa_roles
(
  role_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '角色ID',
  role_name			VARCHAR(40)		NOT NULL DEFAULT '' COMMENT '角色名称',
  role_type			TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '角色类型,参见系统参数表',
  remark			VARCHAR(100)	NOT NULL DEFAULT '' COMMENT '描述',

  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,0-正常、1-已删除',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (role_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='角色表';

INSERT INTO exa_roles(role_id,role_name,role_type) VALUES(1,'系统管理员',1);
INSERT INTO exa_roles(role_id,role_name,role_type) VALUES(2,'账号管理员',2);
INSERT INTO exa_roles(role_id,role_name,role_type) VALUES(3,'运维人员',3);

-- ----------------------------
-- Table structure for exa_role_func_rights
-- 角色和功能权限关系表
-- 角色和功能权限是多对多关系
-- ----------------------------
DROP TABLE IF EXISTS exa_role_func_rights;
CREATE TABLE exa_role_func_rights
(
  role_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '角色ID',
  func_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '功能ID',
  sub_full_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '是否包含全部子节点权限,0-否,1-是',

  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录删除标记,保留字段',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME 		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (role_id, func_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='角色和功能权限关系表';  
​	功能表的URL字段,用于后端访问控制,一般使用AOP来实现。前端使用dom_key字段(即element的id属性),对于Vue等单页面框架,不同模块可以重复使用相同的dom_key值,如新增按钮都使用"add";对于非单页面框架,如bootstrap的js框架等,要注意确保dom_key值的唯一性。
​	关于Vue前端的访问控制,参见:<a href="https://www.cnblogs.com/alabo1999/p/14960584.html" target="_blank">Vue前端访问控制方案</a>和<a href="https://www.cnblogs.com/alabo1999/p/14976481.html" target="_blank">Vue 前端权限控制的优化改进版</a>。
​	功能权限管理与CRUD关系不大,各个模块无需维护。

3.6.2、数据权限管理

​	对于某项功能,如果有功能权限时,所有用户看到或操作的数据集如果是一样的,则无需数据权限控制,只需要功能权限控制即可。如果不同用户,使用相同的功能,可访问的数据集不同,则需要数据权限。
​	数据权限是用户可以观察和操作的数据集的权限,这个也称为RBAC(Resource-Based Access Control),即基于资源的访问控制,为了区别于基于角色的访问控制,我将之定义为R2BAC。
​	如果不同组织有各自的账号管理员,其可以分配管理自己组织的用户账号,但不能看到和操作其它组织的用户账号,这就是数据权限的典型例子。
​	数据权限,有“功能+行+列”3个维度。功能维度,即不同操作功能可访问的数据集不同,如某类账号管理员可以查询本组织的所有账号,但只能编辑由其创建的那些账号。行维度,为数据记录集合,即可以访问哪些记录。列维度,为字段集合,如对某些用户,可以看到全部字段;对另外一些用户,某些字段不显示。
​	功能维度,对于大部分应用,不需要,即增删改查的数据集是相同的(撇开功能权限)。如需要,配置数据权限的功能ID。
​	行维度,即数据筛选,实际上是定义数据过滤的条件。数据过滤条件可以非常复杂,复杂的过滤条件一般直接用代码层面控制,而常用的数据过滤条件一般是记录的某个或某几个ID字段的取值范围,相当于sql语句的"in (...)",下面给出的设计方案是ID字段的行维度数据权限访问控制。
​	列维度,可以用数据权限字段过滤表来定义,但由于不大常用,一般也不用,而是直接用代码层面控制,将相关过滤的属性值设置为null即可。
​	在CRUD代码实现时,需要加入数据权限的校验和过滤。如对组织的账号管理员,新增时,需要判断orgId是否在许可集中;编辑时,原记录的orgId是否在许可集中,如修改了orgId,新的orgId是否在许可集中;删除时,需要判断orgId是否在许可集中;查询时,需要将权限许可的orgIdList加入查询条件,或查询后,检查orgId是否在许可集中。
​	下面是数据权限行维度访问控制的相关表:
-- ----------------------------
-- Table structure for exa_dr_fields
-- 数据权限相关字段表
-- 为简化处理,要求权限相关字段,在所有表中具有相同的定义和含义
-- ----------------------------
DROP TABLE IF EXISTS exa_dr_fields;
CREATE TABLE exa_dr_fields
(
  field_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '字段ID',
  field_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '字段名称',
  prop_name			VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '属性名称',
  invalid_value		VARCHAR(20)		NOT NULL DEFAULT '' COMMENT '无效值,用于ID字段无权限时的查询条件',
  has_sub			TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '是否有下级对象,0-否,1-是',
  is_user_prop		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '是否为用户属性字段,0-否,1-是',
  is_id				TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '是否为ID字段,即起源表的主键,0-否,1-是',
  remark			VARCHAR(200)	NOT NULL DEFAULT '' COMMENT '备注',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录标记,,0-正常、1-已删除',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (field_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='数据权限相关字段表';

INSERT INTO exa_dr_fields(field_id,field_name,prop_name,invalid_value,has_sub,is_user_prop,is_id) 
VALUES(1,'org_id','orgId','-1',1,1,1);

-- ----------------------------
-- Table structure for exa_user_drs
-- 用户数据权限表
-- 用户对各个权限相关字段,应都有一条记录,如无记录,表示无权限
-- ----------------------------
DROP TABLE IF EXISTS exa_user_drs;
CREATE TABLE exa_user_drs
(
  user_id			BIGINT(20)		NOT NULL DEFAULT 0  COMMENT '用户ID',
  field_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '字段ID',
  field_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '字段名',
  -- 如果字段为用户属性字段,则允许使用默认规则,否则不允许
  dr_type			TINYINT(4)		NOT NULL DEFAULT 1  COMMENT '数据权限类型,1-默认规则、2-自定义、3-全部',
  -- 如果字段不是ID类型,可以使用表达式,目前暂时不用
  expr				VARCHAR(255)	NOT NULL DEFAULT '' COMMENT '表达式',
  func_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '功能ID,0-所有功能',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录标记,保留',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (user_id,field_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='数据权限规则表';

-- ----------------------------
-- Table structure for exa_user_custom_drs
-- 用户自定义数据权限表
-- 仅针对数据权限类型为自定义的用户数据权限记录
-- 由于不确定有多少个自定义权限字段,因此使用了记录ID作为主键,方便增加字段
-- ----------------------------
DROP TABLE IF EXISTS exa_user_custom_drs;
CREATE TABLE exa_user_custom_drs
(
  user_id			BIGINT(20)		NOT NULL DEFAULT 0  COMMENT '用户ID',
  field_id			INT(11)			NOT NULL DEFAULT 0  COMMENT '字段ID',
  field_value		INT(11)			NOT NULL DEFAULT 0  COMMENT '字段值',

  -- 记录操作信息
  operator_name		VARCHAR(80)		NOT NULL DEFAULT '' COMMENT '操作人账号',
  delete_flag		TINYINT(4)		NOT NULL DEFAULT 0  COMMENT '记录标记,保留',
  create_time		DATETIME		NOT NULL DEFAULT NOW() COMMENT '创建时间',
  update_time		DATETIME		DEFAULT NULL ON UPDATE NOW() COMMENT '更新时间',
  PRIMARY KEY (user_id,field_id,field_value)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT ='用户数据权限关系表';
​	使用了三个表来管理数据权限,如果需要列维度的数据权限,则还需要增加数据权限字段过滤表。
​	数据权限相关字段表exa_dr_fields,用于定义数据权限相关的字段,包括数据库字段名称,对象属性名。为方便数据权限管理,这个名称和含义是整个系统一致的,如org_id,表示组织ID,则所有表中如包含org_id字段的,其含义必须是组织ID。
​	invalid_value为字段的某个无效值,如ID字段一般大于0,但有时0表示全部,所以一般取-1表示ID的无效值。当对该字段无权限时,使用无效值,如orgIdList为[-1],查询条件为"org_id in (-1)",查询到的结果集为空集,表示无权限。
​	has_sub,表示是否有下级对象,即ID字段代表的对象是否支持树型结构,如组织对象,支持下级对象;而班级对象,则不支持下级对象。为了方便权限设置,如果包含所有下级对象的权限,只需设置父级对象的权限,而不必一一设置。
​	is_user_prop,是否为用户属性字段,如果为用户属性字段(用户表或用户扩展表,用户的某种属性值),则可以使用默认权限。如用户只能属于某一个组织,则org_id为用户属性字段;如果用户可以属于某几个组织,则用户表的组织ID属性失去意义,就不能使用默认数据权限。
​	is_id,是否为ID字段,即起源表的主键,即为某种对象的ID。目前所有权限字段都是ID字段,该字段是为了保留扩展的可能性(如需要增加数据权限字段过滤表,则过滤字段一般就不是ID字段)。

​	用户数据权限表exa_user_drs,用于定义对不同数据权限字段和功能ID的数据权限类型。
​	dr_type,数据权限类型,1-默认规则、2-自定义、3-全部。如果字段为用户属性字段,则可以使用“默认规则”。如用户只能属于某一个组织,则org_id为用户属性字段,默认规则就是用户可以访问本组织及下属组织的所有数据。“自定义”,就是权限范围通过用户自定义数据权限表exa_user_custom_drs来配置,如未配置,则无数据权限(树型对象默认包含全部下级对象,下级对象可不配置)。“全部”,就是对此字段不进行过滤。数据权限类型支持默认规则和全部,可以方便数据配置,减少维护工作量。如支持默认规则,则本组织下级增加一个子组织,该用户自动拥有对新增子组织的访问权限,而无需在用户自定义数据权限表中配置。
​	func_id,功能ID,用于功能维度,0表示全部。每个用户,在每个权限字段上,都需要配置一条数据权限记录,功能ID为0,用于不特别指定功能ID的情况下的数据权限配置。如某些功能需要特别数据权限,则再加上特定功能ID的数据权限记录。
​	expr,用于sql条件表达式,为扩展保留,未使用。

​	用户自定义数据权限表exa_user_custom_drs,用于数据权限类型为自定义时,ID字段的取值范围。
​	field_value,字段值,不同ID字段的数据类型,此处使用整数型,如有Long型,则需要修改字段数据类型。

​	假设系统需要进行数据权限控制的字段集为A={d1,d2,...,dn},某个表T如需要进行数据权限控制,则其需要包含A的某个非空子集的字段。如果表T包括多个数据权限字段,则数据权限为交集,相当于"d1 in (...) and d2 in (...)"。为了方便查询过滤,表结构设计时,应该增加相关数据权限字段的冗余设计。这样查询T时,可以使用如:"di in (...)"形式的查询条件,而不必进行表的联结,并且冗余设计后,由于实体类对象包含了相关数据权限字段,也便于代码实现。

4、Entity实体类 ​

4.1、实体类代码示例

​	Entity实体类的代码示例如下:
package com.abc.example.entity;

import java.time.LocalDate;
import java.time.LocalDateTime;
import javax.persistence.Column;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

/**
 * @className	: User
 * @description	: 用户对象实体类
 * @summary		: 
 * @history		:
 * ------------------------------------------------------------------------------
 * date			version		modifier		remarks
 * ------------------------------------------------------------------------------
 * yyyy/mm/dd	1.0.0		author		    初版
 *
 */
@Data
public class User {
	// 用户ID
	@Column(name = "user_id")
	@JsonFormat(shape= JsonFormat.Shape.STRING)
	private Long userId = 0L;
	
	// 用户名
	@Column(name = "user_name")
	private String userName = "";
	
	// 用户密码
	@Column(name = "password")
	private String password = "";
	
	// 加盐md5算法中的盐
	@Column(name = "salt")
	private String salt = "";
	
	// 用户类型,1-系统管理员、2-公司内部用户、3-外部用户
	@Column(name = "user_type")
	private Byte userType = 3;
	
	// 组织机构ID
	@Column(name = "org_id")
	private Integer orgId = 0;
	
	// 组织名称
	private String orgName = "";	
	
	// 真实姓名
	@Column(name = "real_name")
	private String realName = "";
	
	// Email
	@Column(name = "email")
	private String email = "";
	
	// 手机号码
	@Column(name = "phone_number")
	private String phoneNumber = "";
	
	// 性别,1-无值、2-男、3-女、4-其它
	@Column(name = "sex")
	private Byte sex = 1;
	
	// 生日
	@Column(name = "birth")
	@JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
	private LocalDate birth;
	
	// 身份证号码
	@Column(name = "id_no")
	private String idNo = "";
	
	// 微信小程序的openid
	@Column(name = "open_id")
	private String openId = "";
	
	// 微信公众号openid
	@Column(name = "woa_openid")
	private String woaOpenid = "";
	
	// 备注
	@Column(name = "remark")
	private String remark = "";
	
	// 操作人账号
	@Column(name = "operator_name")
	private String operatorName = "";
	
	// 记录删除标记,0-正常、1-禁用
	@Column(name = "delete_flag")
	private Byte deleteFlag = 0;
	
	// 创建时间
	@Column(name = "create_time")
	@JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
	private LocalDateTime createTime;
	
	// 更新时间
	@Column(name = "update_time")
	@JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
	private LocalDateTime updateTime;	

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (userName == null) {
			if (other.userName != null)
				return false;
		} else if (!userName.equals(other.userName))
			return false;
		return true;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((userName == null) ? 0 : userName.hashCode());
		return result;
	}	
}    

4.2、实体类开发规范

​	1)一般使用lombok的@Data注解,以替代getter/setter方法的冗长代码。
​	2)属性字段名使用驼峰规则,要有对应的数据表字段名,以及属性的描述。数据表字段名可使用JPA的@Column来表述,其它添加属性字段则不需要@Column注解。
​	3)默认值,这个要与表结构一致,否则实现Mybatis脚本insert语句会比较复杂。
​	4)不同数据类型的前端支持问题,如LocalDateTime的格式化问题,前端Long型数据损失精度问题等,这些需要用@JsonFormat进行注解。
​	5)与数据库字段对应的属性字段,其数据类型使用类的形式,如Integer,Long,而不是int,long,这样可以赋值为null。考虑到信息安全及数据权限,某些属性不希望展示给前端,此时可以设置为null。需要注意的是,数据类型使用类的形式,类对象之间比较,==和!=操作符会失效,要使用equals方法来比较值。
​	6)是否需要支持对象克隆,如需支持,需要实现Cloneable的clone接口,支持克隆可大大简化对象复制的处理代码。
​	7)是否需要支持对象比较,如果实体类对象要作为Map的key,则需要实现equals和hashCode接口,这个可以使用IDE工具自动生成代码。
​	8)是否需要支持格式化输出,如作为TreeNode的节点数据,则需要实现toString接口。
​	9)是否需要增加其它表的引用字段,如orgId,需要引用组织表的orgName字段,否则前端不好展示。
​	10)是否有一些内部字段,不希望传到前端,可考虑使用@JsonIgnore进行注解。
​	11)考虑线程安全,日期时间类型不再使用Date类型,而是使用LocalDateTime,LocalDate,LocalTime。另外,关于时间,涉及到时区问题,关于Mysql驱动的配置项应考虑GMT+8,即:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/demodb?verifyServerCertificate=false&useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
​	12)是否需要支持Excel导入,如需支持,应增加一个equals和hashCode方法,作为在导入数据时的异常数据行的定位,这个在后面Excel导入功能时将详细讨论。

(未完待续...)

文章来源地址https://www.toymoban.com/news/detail-478860.html

到了这里,关于Spring Boot实现高质量的CRUD-1的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • AI Code Translator —— 能够实现高质量的自动编程语言转换工具

    https://github.com/mckaywrigley/ai-code-translator AI Code Translator —— 是一款基于大型语言模型的代码翻译工具,同时也是一款颠覆性的编程语言翻译工具,它基于先进的机器学习技术和大规模语料库训练而成,能够实现高质量的自动编程语言转换。 这款工具最大的突破在于翻译速度快且

    2024年02月04日
    浏览(49)
  • 如何写出高质量代码?

    作为一名资深开发人员,写出高质量的代码是我们必须要追求的目标。然而,在实际开发中,我们常常会遇到各种问题。比如,代码的可读性、可维护性、健壮性和灵活性等,这些都会影响代码的质量。那么,究竟如何才能写出高质量的代码呢? 代码结构清晰易懂,能够使代

    2024年02月02日
    浏览(55)
  • 如何写出高质量代码

    一、 前言 编写高质量代码是每一位程序员的追求。高质量的代码可以提高代码可读性、可维护性、可扩展性以及软件运行的性能和稳定性。在这篇文章中,我将分享一些编写高质量代码的特征、编程实践技巧和软件工程方法论。 可读性:好的代码应该能够被维护者轻易地理

    2024年02月02日
    浏览(85)
  • 如何编写高质量代码

    现代软件开发中,代码是构建高质量软件的核心。高质量代码能够提高软件系统的可靠性、可维护性和可扩展性,减少bug的数量和修复时间,提升开发效率和代码可读性,同时有助于团队协作和知识传承共享。 然而,梦想是丰满的,现实是骨感的!软件开发面临诸多挑战。

    2024年02月02日
    浏览(119)
  • 高质量椭圆检测库

    目录 前言 效果展示 检测库 简介 安装库 用法 测试 论文算法步骤简读 1. lsd 检测 2. lsd group 3. 生成初始 ellipse 4. 聚类 椭圆检测是工业中比较常用的一种检测需求。目前常用的基于传统图像处理的椭圆检测方法是霍夫变换,但是霍变换的检测率比较低,很难满足工业场景。而基

    2024年02月07日
    浏览(147)
  • 网络安全高质量文库

    PeiQI文库 http://api.orchidstudio.cn/ PeiQi文库是一个面对网络安全从业者的知识库,涉及漏洞研究,代码审计,CTF夺旗,红蓝对抗等多个安全方向,用于解决安全信息不聚合,安全资料不易找的难题。帮助网络安全从业者共同构建安全的互联网,快速验证并及时修复相关漏洞,为甲

    2024年02月12日
    浏览(52)
  • 如何写出高质量的代码

    你是否曾经为自己写的代码而感到懊恼?你是否想过如何才能写出高质量代码?那就不要错过这个话题!在这里,我们可以讨论什么是高质量代码,如何写出高质量代码等问题。无论你是初学者还是资深开发人员,都可以在这个话题下进行分享,汲取灵感和知识,共同提高自

    2023年04月25日
    浏览(126)
  • Visio 转为高质量PDF

    Visio另存为pdf不够清晰怎么办 - - 可以选择先另存为高分辨率的图片( 存的时候分辨率选择打印机或者自定义即可 ),然后转为pdf. 或者用 打印 1 保存为高质量 2 的pdf (本文介绍) 版本:Microsoft Visio 2010 Adobe Acrobat 2018 关键就是设置分辨率,不按照以上流程亦可 一般情况下安装完

    2024年02月04日
    浏览(48)
  • 如何编写高质量的测试计划

    1.1目的 简述本计划的目的,旨在说明各种测试阶段任务、人员分配和时间安排、工作规范等。 测试计划在策略和方法的高度说明如何计划、组织和管理测试项目。测试计划包含足够的信息使测试人员明白项目需要做什么是如何运作的。另外,清晰的文档结构能使任何一个读

    2024年02月16日
    浏览(53)
  • 有哪些高质量的自学网站?

    分享32个鲜为人知并且完全免费的高质量自学网站,每个都是堪称神器,让你相见恨晚。 是一个完全免费的综合视频教程网站,非常良心实用。 它提供的视频教程非常丰富并且质量很高,包括:PS 教程、手机摄影教程、Ai 做图教程、Excel 教程、Word 教程、PPT 教程、Pr 视频剪辑

    2024年02月02日
    浏览(125)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包