业务层、数据权限、数据事务、通用基类、分页逻辑
# 引言
对于业务逻辑层的开发重复代码很多,尽管有代码生成器,但从代码量总的来说还是比较多,所以就有了以下抽象类及工具,对一些常用操作进行封装。
对通用新增、删除、编辑、查询,代码操作进行封装简化,减少运维成本。您只需要写您的业务逻辑代码就可以了。
对特有树状结构特有字段如(所有父级编码、所有排序号编码、是否是叶子节点、当前节点层次)进行更新,比如,通过所有父级编码可快速查询到所有子级的数据;通过所有排序号,可快速对整个树结构进行排序;通过是否叶子节点快速得知是否有下级;根据当前层次快速知道当前节点在树中的级别。
对事务处理使用 Spring 事务 @Transactional 注解,进行方法级别的事务控制,不用单独处理事务及回滚。如配置传播行为,进行事务继承,子事务,事务回滚行为等,配置隔离级别读取未提交的数据等。
# 数据权限
对通用数据权限进行简化封装,可以配置人员与数据权限、角色与数据权限的定制(角色上的数据权限与人员身上的数据权限为或者关系,两者的并集)。 数据权限不仅仅支持公司、部门、角色,还可以通过配置支持您的业务字段数据信息过滤,如订单的区域、内容管理的栏目、故障单类型等等。
无需让您多写代码,根据界面化配置规则,通过代码简单调用,即可实现复杂的数据权限控制。
支持控制业务范围,根据功能划分权限,有的可以看本部门的,有的可以看本公司的数据。
拥有的权限和管理的权限划分,方便不同人员岗位权限区分(专为管理员独有的设计)。
数据权限实现原理是通过 Exists 或 Join 的方式进行 SQL 数据权限过滤,在分库的情况下,或微服务 Cloud 环境情况下,不方便进行联表查询操作(如:跨数据库、跨应用、微服务)可使用 API 方式,进行数据权限过滤。
数据权限是对数据行级别的权限控制,针对于字段级别权限请关注功能权限章节。
# 角色数据范围
角色数据范围支持 “本部门”、“本公司”、“本部门和本公司”、“自定义数据” 的权限控制。 这些权限可以在 application.yml 里进行灵活配置,根据业务需要,您还可以扩展,如 “本行业”、“本区域” 等。
指定的数据权限范围类型,若多个角色同时指定,之间为或者关系;人员权限与角色权限之间同样为或者关系,多个位置设置权限,依照最大权限为主。
- 本人数据(未设置):忽略这个角色的数据权限设置
- 全部数据:可以查看全部数据,无需控制权限(最大权限)
- 自定义数据:打对勾,跨部门、跨机构的情况设置数据权限
- 本部门数据:仅控制当前用户所在部门(所在机构)的数据权限
- 本公司数据:仅控制当前用户所在公司的数据权限
- 本部门和本公司:控制当前用户所在部门和所在公司的数据权限
- 自定义数据范围类型:修改
role.extendDataScopes
参数配置
注意:如果用户的所有角色及用户数据权限都未设置,则只可查询本人数据(自己创建的)
# 控制业务范围
控制业务范围,可以分功能、分模块的去控制数据权限。该功能,适应于:v4.1.6+
当您使用 addFilter 权限过滤的时候可以指定适应的业务范围 bizScope,不指定代表所有生效。
设置业务范围的角色就是仅对这个业务生效的,未设置的就是不限制某个业务权限控制,则全部业务生效。
bizScope 与字典管理中的字典类型 sys_role_biz_scope 进行匹配,也可增加自定义业务范围类型。
// 最后一个参数是 bizScope,指定的 office_user 对应角色里选择的业务范围字典编码
SqlMap sqlMap = empUser.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = empUser.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getDataScope().addFilter("dsfOffice",
"Office", "e.office_code", "a.create_by", ctrlPermi, "office_user");
2
3
4
5
业务场景如:有的功能可以看本部门,有的功能可以看本公司,有的功能可以看全部数据。
# 菜单数据权限
这是一个相比控制业务范围更细化的一个权限控制(v5.10.1),根据菜单按钮配置数据权限参数, 真正做到,某一个功能、菜单、接口或按钮实现不同的权限控制,包括 3 种类型:
1、通用数据权限:引用角色数据权限配置,如:全部数据、自定义数据、本部门数据、本公司数据、本部门和本公司数据等。
2、自定义条件规则:一系列的查询条件规则配置,支持上下级嵌套查询条件,包括:
- 关系:AND、OR、使用括号嵌套时,增加下级节点,以第一个关系规则作为括号前面的关系
- 字段名:指定数据库字段名,支持 JOIN 列,如:a.user_code
- 条件:=、!=、>、>=、<=、IN、NOT IN、包含、不包含、开始以、不开始以、结束以、不结束以、是空、不是空
- 匹配值:支持动态表达式获取,如:当前用户对象、会话、实体、缓存、配置
3、自定义SQL片段:可手写SQL语句,注意需后端配置文件开启 user.dataScopeRuleSql
参数后,才可使用该功能。
配置方法
菜单:角色管理 -> 数据权限 -> 菜单数据权限
首先选择一个菜单或权限按钮(必须预先配置权限标识),然后在右侧选择以上 3 中权限类型的一种,进行配置规则
调用方法
接口通过菜单权限标识获取菜单编码,如有多个菜单指定相同的权限标识,则会读取到多个菜单配置的数据权限规则,例如:
// 可查看哪些部门数据,第二个参数:指定菜单中的 “权限标识”
sqlMap.getDataScope().addFilterByPermission("dsfOffice", "sys:empUser:view",
"Office", "e.office_code", "a.create_by", ctrlPermi);
// 可查看哪些用户数据,第二个参数:指定菜单中的 “权限标识”
sqlMap.getDataScope().addFilterByPermission("dsfOffice", "sys:empUser:view",
"User", "a.user_code", ctrlPermi);
2
3
4
5
6
# 自定义数据权限
主要包括 “人员与数据权限” 和 “角色与数据权限”,即:不仅支持角色与数据权限定制,我们将颗粒度还细化人员身上,支持人员与数据权限的配置,配置菜单如下:
- 人员与数据权限:用户管理 -> 用户列表 -> 数据权限
- 角色与数据权限:角色管理 -> 角色列表 -> 数据权限
控制数据主要包括这两张表:js_sys_role_data_scope、js_sys_user_data_scope,表字段的含义如下:
- 控制类型:Office:部门;Company:公司、Role:角色;
- 控制数据:被控制数据权限的数据主键编号,业务表的主键编号;
- 控制权限:控制权限主要分为两大类,如下:
- 拥有的权限(DataScope.CTRL_PERMI_HAVE):当前人员可以访问某些业务数据的权限控制。
- 管理的权限(DataScope.CTRL_PERMI_MANAGE):当前人员可以管理的数据权限,主要用于二级管理员身份的权限,如二级管理可以给某个人员分配部门经理角色,但他不能访问部门经理的业务数据。
角色上配置的数据权限与人员身上的数据权限为或者关系,即:如果两者都配置了相应的权限,则都会生效。
数据权限不仅仅支持公司、部门、角色,还可以通过 application.yml 里的一些配置,很容易的进行扩充,业务字段数据信息过滤,如订单的区域、内容管理的栏目、故障单类型等等,修改 user.dataScopes
参数配置。
点我查看 application.yml 数据权限配置参数详情
# ★ 使用方法 ★
# 1、第一步
/**
* 添加数据权限过滤条件
*/
public void addDataScopeFilter(T entity){
SqlMap sqlMap = entity.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = entity.sqlMap(); // v5.3.0+ 及之后版本
// 举例1:公司数据权限过滤,实体类@Table注解extWhereKeys="dsf"
sqlMap.getDataScope().addFilter("dsf", "Company",
"a.company_code", DataScope.CTRL_PERMI_HAVE);
// 举例2:部门数据权限过滤,实体类@Table注解extWhereKeys="dsf"
sqlMap.getDataScope().addFilter("dsf", "Office",
"a.office_code", DataScope.CTRL_PERMI_HAVE);
// 举例3:角色数据权限过滤,实体类@Table注解extWhereKeys="dsf"
sqlMap.getDataScope().addFilter("dsf", "Role",
"a.role_code", DataScope.CTRL_PERMI_HAVE);
// 举例4:用户、员工(自己创建的)数据权限根据部门过滤,实体类@Table注解extWhereKeys="dsfOffice"
sqlMap.getDataScope().addFilter("dsfOffice", "Office",
"e.office_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);
// 举例5:用户、员工(自己创建的)数据权限根据公司过滤,实体类@Table注解extWhereKeys="dsfCompany"
sqlMap.getDataScope().addFilter("dsfCompany", "Company",
"e.company_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
注意:在调用 findList 或 findPage 之前去手动调用 addDataScopeFilter 方法,才可生效。例如:
@RequiresPermissions("user")
@RequestMapping(value = "listData")
@ResponseBody
public Page<EmpUser> listData(EmpUser empUser, HttpServletRequest request, HttpServletResponse response) {
empUser.setPage(new Page<>(request, response));
empUserService.addDataScopeFilter(empUser); // 调用数据权限过滤方法(重点)
Page<EmpUser> page = empUserService.findPage(empUser);
return page;
}
2
3
4
5
6
7
8
9
# 2、第二步
1)在 @Table 注解中调用如下:
- 采用 EXISTS 方式调用 :
@Table(extWhereKeys="dsf")
- 采用 JOIN 方式调用 :
@Table(extFromKeys="dsfFrom",extWhereKeys="dsfWhere")
2)MyBatis Mapper 中调用如下两种方式:
- 采用 EXISTS 方式调用 : 将
${sqlMap.dsf}
放在Where
后 - 采用 JOIN 方式调用 : 将
${sqlMap.dsfFrom}
放在From
后 ,将${sqlMap.dsfWhere}
放在Where
后
# 3、第三步
给予角色或用户授权数据,进入菜单:
- 系统管理 -> 权限管理 -> 角色管理 -> 数据权限 -> 角色数据范围
- 系统管理 -> 组织管理 -> 用户管理 -> 数据权限 -> 用户数据权限
# 常见问题
- 如果遇到
where 1=2
的问题,说明当前用户没有数据权限的问题。如果是调用的机构树、公司树等,您可以添加isAll=true
参数查询全部数据。 - 用户管理或机构管理查询不到数据,因为用户管理和机构管理的数据权限被定义为管理的权限(所以需要从 “二级管理员 -> 新增 -> 可管理的数据权限” 里授权设置),如果采用无限级授权(即只能创建本部门的用户,只能分配自己拥有的角色,只能分配自己拥有的菜单),这时您需要将用户管理的权限改为拥有的权限,application.yml 设置
user.adminCtrlPermi=1
(v4.1.5+)即可。
# 扩展数据权限
若 JeeSite 自带的数据权限不能满足您的要求,您也可以在此基础上扩展自己的数据权限。
/**
* 添加数据权限过滤条件
*/
public void addDataScopeFilter(T entity){
SqlMap sqlMap = entity.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = entity.sqlMap(); // v5.3.0+ 及之后版本
// 添加数据权限过滤条件
sqlMap.getDataScope().addFilter("dsfOffice", "Office",
"e.office_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);
// 添加自定义权限过滤条件(或者包含所有的部门经理)
sqlMap.getDataScope().addFilter("dsfOffice",
"EXISTS (SELECT 1 FROM js_sys_user_role WHERE user_code=a.user_code and role_code='dept')");
// 若不需要此数据权限了,也可以进行根据 key 进行清理权限数据
sqlMap.getDataScope().clearFilter("dsfOffice");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上述,使用 addFilter 新追加的过滤条件,与之前追加的 dsfOffice 过滤条件为 OR 关系。
# 跨库数据权限
支持数据源跨库,但必须在一个数据库实例下,并设置其它数据源的用户可以访问默认数据源表的权限。 举例:数据库 Schema(模式、用户名)为 jeesite 则设置如下:
# 数据库连接
jdbc:
# 表名前缀
tablePrefix: jeesite.js_
2
3
4
# 数据权限参数
# 用户管理
user:
# 自定义数据权限,moduleCode: 针对模块, ctrlPermi: 权限类型(0全部 1拥有权限 2管理权限)
dataScopes: >
[{
moduleCode: "core",
ctrlPermi: "0",
ctrlName: "机构权限",
ctrlName_en: "Office",
ctrlType: "Office",
ctrlDataUrl: "/sys/office/treeData",
chkboxType: {"Y":"ps","N":"ps"},
expandLevel: -1,
remarks: ""
},{
moduleCode: "core",
ctrlName: "公司权限",
ctrlName_en: "Company",
ctrlType: "Company",
ctrlPermi: "0",
ctrlDataUrl: "/sys/company/treeData",
chkboxType: {"Y":"ps","N":"ps"},
expandLevel: -1,
remarks: ""
},{
moduleCode: "core",
ctrlName: "用户权限",
ctrlName_en: "User",
ctrlType: "User",
ctrlPermi: "0",
ctrlDataUrl: "/sys/office/treeData?isLoadUser=true",
chkboxType: {"Y":"s","N":"s"},
expandLevel: -1,
remarks: ""
},{
moduleCode: "core",
ctrlName: "角色权限",
ctrlName_en: "Role",
ctrlType: "Role",
ctrlPermi: "2",
ctrlDataUrl: "/sys/role/treeData",
chkboxType: {"Y":"ps","N":"ps"},
expandLevel: -1,
remarks: ""
},{
moduleCode: "cms",
ctrlName: "栏目权限",
ctrlName_en: "Category",
ctrlType: "Category",
ctrlPermi: "0",
ctrlDataUrl: "/cms/category/treeData",
chkboxType: {"Y":"ps","N":"ps"},
expandLevel: -1,
remarks: ""
}]
# 数据权限调试模式(会输出一些日志)
dataScopeDebug: false
# 数据权限使用 API 方式实现(适应 Cloud 环境,基础用户表与业务数据表跨库的情况)
# 开启后设置 ctrlDataAttrName 加 AndChildren 后缀,ctrlDataParentCodesAttrName 清空
# 以方便读取树结构数据权限的表时包含子节点,举例如下:
# ctrlDataAttrName: "officeCodesAndChildren", ctrlDataParentCodesAttrName: ""
dataScopeApiMode: false
# v5.10.1 开始默认关闭 JOIN 模式的数据权限,如有需要可打开此参数
dataScopeJoinMode: false
# 菜单数据权限,是否启用自定义 SQL 执行权限 v5.10.1
dataScopeRuleSql: false
# 角色管理
role:
# 扩展数据权限定义:3:本部门;4:本公司;5:本部门和本公司
extendDataScopes: >
{
3: {
Office: {
#控制类型的类名 : "用来获取控制表名和主键字段名,如果为 NONE,则代表是不控制该类型权限",
ctrlTypeClass: "com.jeesite.modules.sys.entity.Office",
#控制数据的类名: "指定一个静态类名,方便 ctrlDataAttrName 得到权限数据,如:当前机构编码、当前公司编码、当前行业编码等",
ctrlDataClass: "com.jeesite.modules.sys.utils.EmpUtils",
#控制数据的类名下的属性名 : "可看做 ctrlDataClass 下的 get 方法,如:EmpUtils.getOfficeCodes(),支持返回字符串或字符串数组类型",
ctrlDataAttrName: "officeCodes",
#控制数据的所有上级编码 : "用于控制数据为树表的情况,为数组时,必须与 ctrlDataAttrName 返回的长度相同,不是树表设置为空",
ctrlDataParentCodesAttrName: "officeParentCodess"
},
Company: {
ctrlTypeClass: "NONE"
}
},
4: {
Office: {
ctrlTypeClass: "NONE"
},
Company: {
ctrlTypeClass: "com.jeesite.modules.sys.entity.Company",
ctrlDataClass: "com.jeesite.modules.sys.utils.EmpUtils",
ctrlDataAttrName: "company.companyCode",
ctrlDataParentCodesAttrName: "company.parentCodes"
}
},
5: {
Office: {
ctrlTypeClass: "com.jeesite.modules.sys.entity.Office",
ctrlDataClass: "com.jeesite.modules.sys.utils.EmpUtils",
ctrlDataAttrName: "officeCodes",
ctrlDataParentCodesAttrName: "officeParentCodess"
},
Company: {
ctrlTypeClass: "com.jeesite.modules.sys.entity.Company",
ctrlDataClass: "com.jeesite.modules.sys.utils.EmpUtils",
ctrlDataAttrName: "company.companyCode",
ctrlDataParentCodesAttrName: "company.parentCodes"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 附:API
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:用户对象、用户角色、控制用户字段、业务范围、实现方式(exists、join、cloud)
* @param sqlMapKey sqlMap的键值,举例:如设置“dsf”数据范围过滤,则:<br>
* exists方式对应:sqlMap.dsf; join方式对应:sqlMap.dsfFrom 和 sqlMap.dsfWhere
* @param ctrlTypes 控制类型,多个用“,”隔开,多个是“or”关系,举例:<br>
* 控制角色:Role<br>
* 控制部门:Office<br>
* 控制公司:Company
* @param bizCtrlDataFields 业务表对应过滤表别名和加权字段,多个使用“,”分隔。<br>
* 长度必须与 ctrlTypes 保持一致,举例:<br>
* 业务表控制角色:a.role_code<br>
* 业务表控制部门:a.office_code<br>
* 业务表控制公司:a.company_code
* @param bizCtrlUserField 业务表对应过滤表别名和用户字段,用于过滤只可以查看本人数据。<br>
* 不设置的话,如果没有范围权限,则查不到任何数据,举例:<br>
* 业务表:a.create_by
* @param ctrlPermi 拥有的数据权限:DataScope.CTRL_PERMI_HAVE、可管理的数据权限:DataScope.CTRL_PERMI_HAVE
* @param bizScope 业务范围,多个用“,”隔开 v4.1.6
* @param apiMode 是否 API 方式实现(适应 Cloud 环境,基础用户表与业务数据表跨库的情况)
* @param user 用户对象 userCode、roleList(roleCode、dataScope、bizScope)
* @param roleList 角色列表 roleCode、dataScope、bizScope v5.10.1
* @see-example
* 1)在Service中调用如下两种方式:<br>
* // 添加数据权限过滤条件(控制角色)<br>
* entity.sqlMap().getDataScope().addFilter("dsf", "Role", <br>
* "a.role_code", DataScope.CTRL_PERMI_HAVE);<br>
* // 添加数据权限过滤条件(控制部门)<br>
* entity.sqlMap().getDataScope().addFilter("dsf", "Office", <br>
* "a.office_code", DataScope.CTRL_PERMI_HAVE);<br>
* // 添加数据权限过滤条件(控制公司)<br>
* entity.sqlMap().getDataScope().addFilter("dsf", "Company", <br>
* "a.company_code", DataScope.CTRL_PERMI_HAVE);<br>
* // 添加数据权限过滤条件(如果没有部门权限,则控制当前用户)<br>
* entity.sqlMap().getDataScope().addFilter("dsf", "Office", <br>
* "a.office_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);<br>
* // 添加数据权限过滤条件(控制用户)<br>
* entity.sqlMap().getDataScope().addFilter("dsf", "User", <br>
* "a.user_code", DataScope.CTRL_PERMI_HAVE);<br>
* 2)在 \@Table 注解中调用如下:<br>
* 采用 EXISTS 或 API 方式调用 : \@Table(extWhereKeys="dsf")<br>
* 采用 JOIN 方式调用 : \@Table(extFromKeys="dsfFrom", extWhereKeys="dsfWhere")
* 3)MyBatis Mapper 中调用如下两种方式:<br>
* 采用 EXISTS 或 API 方式调用 : 将 ${sqlMap.dsf} 放在Where语句里<br>
* 采用 JOIN 方式调用 : 将 ${sqlMap.dsfFrom} 放在From后 ,将 ${sqlMap.dsfWhere} 放在Where语句里
*/
public QueryDataScope addFilter(String sqlMapKey, User user, List<Role> roleList, String ctrlTypes,
String bizCtrlDataFields, String bizCtrlUserField, String ctrlPermi, String bizScope, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes,
String bizCtrlDataFields, String ctrlPermi);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:控制用户字段
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes,
String bizCtrlDataFields, String bizCtrlUserField, String ctrlPermi);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:控制用户字段、业务范围
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, String bizScope);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:控制用户字段、业务范围
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, String bizScope, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:用户对象、控制用户字段、业务范围
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilter(String sqlMapKey, User user, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, String bizScope, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:菜单权限标识 v5.10.1
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilterByPermission(String sqlMapKey, String permission, String ctrlTypes,
String bizCtrlDataFields, String ctrlPermi);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:菜单权限标识、控制用户字段 v5.10.1
* 详见:{@link QueryDataScope#addFilterByPermission(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilterByPermission(String sqlMapKey, String permission, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:菜单权限标识、控制用户字段、业务范围 v5.10.1
* 详见:{@link QueryDataScope#addFilterByPermission(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilterByPermission(String sqlMapKey, String permission, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:菜单权限标识、控制用户字段、业务范围 v5.10.1
* 详见:{@link QueryDataScope#addFilterByPermission(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilterByPermission(String sqlMapKey, User user, String permission, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)加:菜单权限标识、控制用户字段、业务范围 v5.10.1
* 详见:{@link QueryDataScope#addFilter(String, User, List, String, String, String, String, String, boolean)}
*/
public QueryDataScope addFilterByPermission(String sqlMapKey, User user, List<Role> roleList, String permission, String ctrlTypes, String bizCtrlDataFields,
String bizCtrlUserField, String ctrlPermi, boolean apiMode);
/**
* 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
* @param sqlMapKey sqlMap的键值,举例:如设置“dsf”数据范围过滤,则:exists方式对应:sqlMap.dsf
* @param sqlWhere 具体的Where子句。
*/
public QueryDataScope addFilter(String sqlMapKey, String sqlWhere);
/**
* 清理数据范围过滤条件
* @param sqlMapKey 要清理的数据过滤条件
*/
public QueryDataScope clearFilter(String sqlMapKey);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 数据库事务
事务管理对于企业应用来说是至关重要的,当出现异常情况,它也可以保证数据的一致性。
JeeSite主要使用Spring的@Transactional注解,也称声明式事务管理,是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需通过基于@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。
在并发要求较高的场景JeeSite也支持编程式事务,根据业务强化事务处理。
JeeSite支持分布式JTA事务,或者跨微服务环境下的Seata全局事务。
# 注解属性
属性 | 类型 | 描述 |
---|---|---|
传播性(propagation) | 枚举型 | 可选的传播性设置(默认值:Propagation.REQUIRED) |
隔离性(isolation) | 枚举型 | 可选的隔离性级别(默认值:Isolation.ISOLATION_DEFAULT) |
只读性(readOnly) | 布尔型 | 读写型事务 vs. 只读型事务 |
超时(timeout) | int型 | 事务超时(以秒为单位) |
回滚异常类(rollbackFor) | Class 类的实例,必须是 Throwable 的子类 | 异常类,遇到时进行回滚。特别注意: 默认情况下 checked exceptions 不进行回滚, 仅 unchecked exceptions(即 RuntimeException 的子类)才进行事务回滚。若想 Exception 异常回滚,可设置属性 rollbackFor = Exception.class 即可 |
不回滚异常类(noRollbackFor) | Class 类的实例,必须是Throwable的子类 | 异常类,遇到时不进行回滚。 |
# 事务传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在Propagation定义中包括了如下几个表示传播行为的常量:
- Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务,这是默认值。
- Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
# 事务隔离级别
隔离级别是指若干个并发的事务之间的隔离程度。Isolation 接口中定义了五个表示隔离级别的常量:
- Isolation.DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是Isolation.READ_COMMITTED。
- Isolation.READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
- Isolation.READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
- Isolation.REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
- Isolation.SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
# 事务高级操作
1)如果异常类不是 RuntimeException 的子类,可能事务不会滚动,您可以通过注解指定异常类:
@Transactional(rollbackFor=CustomException.class)
2)有一些情况使用 try...catch... 会使事务失效,您可以可在 catch 中抛出运行时异常或手动回滚:
try{
// 出现异常
} catch (Exception e) {
e.printStackTrace();
// 抛出异常,使得事务回滚
throw new RuntimeException(e);
}
// ======== 或者 ========
try{
// 出现异常
} catch (Exception e) {
e.printStackTrace();
// 手动通知事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3)有一些情况可能不需要全部回滚,您可以设置回滚点:
// 设置回滚点:
Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
// 回滚到 savePoint 指定位置:
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
2
3
4
4)改变 MyBatis 的事务管理机制:
- SpringManagedTransaction(默认):即 MyBatis 本身不去实现事务,而是通过 Spring 进行事务管理
- JdbcTransaction:即利用 java.sql.Connection 去完成对事务的提交、回滚、关闭等
v4.2.3 及之后版本
mybatis:
# 是否开启 JDBC 管理事务,默认 Spring 管理事务 v4.2.3
jdbcTransaction: false
2
3
v4.1.8 到 v4.2.2 版本
jdbc:
# 利用 LCN 开关切换 MyBatis 事务管理机制(false:SpringManagedTransaction;true:JdbcTransaction) v4.1.8
lcn:
enabled: false
2
3
4
也可以在查询方法调用前使用 DataSourceHolder.setJdbcTransaction(true) 对个某个业务操作单独设置 v4.1.9
# 分布式事务
# 数据库连接
jdbc:
# JTA XA 事务,建议启用多数据源并跨数据库的时候开启(v4.0.4+)
jta:
enabled: true
2
3
4
5
# 编程式事务
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
// 编程式事务
public void transTest(Long orderId) {
// 开启事务
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
try {
// 业务处理代码,此处省略...
// 提交事务
platformTransactionManager.commit(transaction);
} catch (Exception e) {
// 回滚事务
platformTransactionManager.rollback(transaction);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 乐观锁
确保修改的数据为最新的,即乐观锁。实现原理:前台提交时间戳作为该表单的版本号,后台通过 @Validated 即可验证版本,提交的最后更新时间早于数据库更新时间,则验证失败。
实现方式,只需一行代码,提交表单增加如下代码(修改 entity 为您的业务实体属性名) :
<input type="hidden" name="lastUpdateDateTime" value="${entity.updateDate.time!}" />
# 服务层基类 API
# 基类及接口的继承关系
服务层:
TreeService -> TreeQueryService -> CrudService -> QueryService -> BaseService
持久层:
TreeDao -> CrudDao -> QueryDao -> BaseDao
实体类:
TreeEntity -> DataEntity -> BaseEntity
题外话
为什么 Controller 没有进行 Crud 封装,因为 Controller 是对外接口,希望程序设计者和后期运维,可以更容易的掌握每一个对外的映射。
# QueryService 查询抽象基类
/**
* 新建实体对象
* @return
*/
protected T newEntity();
/**
* 新建实体对象(带一个String构造参数)
* @return
*/
protected T newEntity(String id);
/**
* 获取单条数据
* @param id 主键
* @return
*/
public T get(String id);
/**
* 获取单条数据
* @param entity
* @return
*/
public T get(T entity);
/**
* 获取单条数据,如果获取不到,则实例化一个空实体
* @param id 主键编号
* @param isNewRecord 如果是新记录,则验证主键编号是否存在。
* 如果存在抛出ValidationException异常。
* @return
*/
public T get(String id, boolean isNewRecord);
/**
* 获取单条数据,如果获取不到,则实例化一个空实体(多个主键情况下调用)
* @param pkClass 主键类型数组
* @param pkValue 主键数据值数组
* @param isNewRecord 如果是新记录,则验证主键编号是否存在。
* 如果存在抛出ValidationException异常。
* @return
*/
public T get(Class<?>[] pkClass, Object[] pkValue, boolean isNewRecord);
/**
* 列表查询数据
* @param entity
* @return
*/
public List<T> findList(T entity);
/**
* 分页查询数据
* @param page 分页对象
* @param entity
* @return
*/
public Page<T> findPage(Page<T> page, T entity);
/**
* 查询列表总数
* @param entity
* @return
*/
public long findCount(T entity);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# CrudService 增删改抽象基类
该类继承QueryService抽象类
/**
* 保存数据(插入或更新)
* @param entity
*/
@Transactional(readOnly = false)
public void save(T entity)
/**
* 插入数据
* @param entity
*/
@Transactional(readOnly = false)
public void insert(T entity);
/**
* 更新数据
* @param entity
*/
@Transactional(readOnly = false)
public void update(T entity);
/**
* 更新状态(级联更新子节点)
* @param entity
*/
@Transactional(readOnly = false)
public void updateStatus(T entity);
/**
* 删除数据
* @param entity
*/
@Transactional(readOnly = false)
public void delete(T entity);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# TreeService 树结构抽象基类
该类继承CrudService抽象类
/**
* 根据父节点获取子节点最后一条记录
*/
public T getLastByParentCode(T entity);
/**
* 保存数据(插入或更新)
* 实现自动保存字段:所有父级编号、所有排序号、是否是叶子节点、节点的层次级别等数据
* 实现级联更新所有子节点数据:同父级自动保存字段
*/
@Transactional(readOnly = false)
public void save(T entity);
/**
* 更新parent_codes、tree_sorts、tree_level字段值
*/
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateParentCodes(T entity);
/**
* 更新当前节点排序号
*/
@Transactional(readOnly = false)
public void updateTreeSort(T entity);
/**
* 更新tree_leaf字段值
*/
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateTreeLeaf(T entity);
/**
* 修正本表树结构的所有父级编号
* 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段
*/
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes();
/**
* 按父级编码修正树结构的所有父级编号
* 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段
*/
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes(String parentCode);
/**
* 预留接口事件,更新子节点
* @param childEntity 当前操作节点的子节点
* @param parentEntity 当前操作节点
*/
protected void updateChildNode(T childEntity, T parentEntity);
/**
* 更新状态(级联更新子节点)
* @param entity
*/
@Transactional(readOnly = false)
public void updateStatus(T entity);
/**
* 删除数据(级联删除子节点)
* @param entity
*/
@Transactional(readOnly = false)
public void delete(T entity);
/**
* 转换为树结构列表形式[code,childList[code,childList[...]]]<br>
* 举例如下:<br>
* List<T> sourceList = service.findList(entity);<br>
* List<T> targetList = service.convertTreeList(sourceList, T.ROOT_CODE);<br>
* @param sourceList 源数据列表
* @param parentCode 目标数据列表的顶级节点
* @return targetList 目标数据列表
*/
public List<T> convertTreeList(List<T> sourceList, String parentCode);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 分页逻辑说明
调用示例如下:
http://host/js/listData?code=123&name=456&pageNo=1&pageSize=20
Controller 部分:
@RequestMapping(value = "listData")
public Page<Custom> listData(Custom custom, HttpServletRequest request, HttpServletResponse response){
custom.setPage(new Page<Custom>(request, response));
Page<Custom> page = service.findListByCodeAndName(custom);
return page;
}
2
3
4
5
6
Service 部分:
public Page<Custom> findPageByCodeAndName(Custom custom){
Page<Custom> page = (Page<Custom>) custom.getPage();
// 不执行Count,不查询总数。
page.setCount(Page.COUNT_NOT_COUNT);
// 只Count数据,不返回数据,通过 page.getCount()获取
page.setCount(Page.COUNT_ONLY_COUNT);
// 不分页,不执行Count,返回全部数据。
custom.setPage(new Page(1, Page.PAGE_SIZE_NOT_PAGING, Page.COUNT_NOT_COUNT));
// 或者
custom.setPage(null);
// 执行查询,获取当前页列表数据和Count数据
page.setList(dao.findListByCodeAndName(custom));
// 获取当前页数据
System.out.println(page.getList());
// 获取总条数
System.out.println(page.getCount());
return page;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Dao 部分:
public List<Custom> findListByCodeAndName(Custom custom);
public Long findListByCodeAndNameCount(Custom custom);
2
Mapper 部分:
<select id="findListByCodeAndName" resultType="Custom">
SELECT * FROM test_data a
<where>
<if test="code != null and code != ''">
AND a.code = #{code}
</if>
<if test="name != null and name != ''">
AND a.name = #{name}
</if>
</where>
<if test="page != null and page.orderBy != null and page.orderBy != ''">
ORDER BY ${page.orderBy}
</if>
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
Entity 部分:
public Custom extends BaseEntity{
private String code;
private String name;
}
2
3
4
# MAP参数分页
Service 部分:
Page<Map<String, Object>> pageMap = new Page<>(1, 20); // 该 Page 可以来自 Controller,例如上节所示。
Map<String, Object> params = MapUtils.newHashMap();
params.put("testInput", "123");
params.put("page", pageMap); // 给 Map 设置 page 参数即可自动分页
pageMap.setList(dao.findListForMap(params));
System.out.println(pageMap.getList()); // 获取当前页数据
System.out.println(pageMap.getCount()); // 获取总条数
2
3
4
5
6
7
Dao 部分:
public List<Map<String, Object>> findListForMap(Map<String, Object> params);
Mapper 部分:
<select id="findListForMap" resultType="map">
SELECT * FROM test_data a
<where>
<if test="testInput != null and testInput != ''">
AND a.test_input = #{testInput}
</if>
</where>
<if test="page != null and page.orderBy != null and page.orderBy != ''">
ORDER BY ${page.orderBy}
</if>
</select>
2
3
4
5
6
7
8
9
10
11
# 自定义分页 Count
有时候,平台内置的 分页 Count 统计不适应业务,也可以自己实现 Count 统计
Controller 部分:
@RequestMapping(value = "listData")
public Page<Custom> listData(Custom custom, HttpServletRequest request, HttpServletResponse response){
custom.setPage(new Page<Custom>(request, response));
Page<Custom> page = service.findListByCodeAndName(custom);
return page;
}
2
3
4
5
6
Service 部分:
public Page<Custom> findPageByCodeAndName(Custom custom){
Page<Custom> page = (Page<Custom>) custom.getPage();
// 执行查询,获取当前页列表数据
page.setCount(Page.COUNT_NOT_COUNT);
page.setList(dao.findListByCodeAndName(custom));
// 获取总条数,并初始化分页对象
custom.setPage(null);
page.setCount(dao.findListByCodeAndNameCount(custom));
page.initialize();
// 获取当前页数据
System.out.println(page.getList());
// 获取总条数
System.out.println(page.getCount());
return page;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dao 部分:
public List<Custom> findListByCodeAndName(Custom custom);
public Long findListByCodeAndNameCount(Custom custom);
2
Mapper 部分:
<sql id="findListByCodeAndNameSql">
test_data a
<where>
<if test="code != null and code != ''">
AND a.code = #{code}
</if>
<if test="name != null and name != ''">
AND a.name = #{name}
</if>
</where>
</sql>
<select id="findListByCodeAndName" resultType="Custom">
SELECT * FROM <include refid="findListByCodeAndNameSql"/>
<if test="page != null and page.orderBy != null and page.orderBy != ''">
ORDER BY ${page.orderBy}
</if>
</select>
<select id="findListByCodeAndNameCount" resultType="long">
SELECT count(1) FROM <include refid="findListByCodeAndNameSql"/>
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Entity 部分:
public Custom extends BaseEntity{
private String code;
private String name;
}
2
3
4
# 覆写内置Service
举例 1:
@Service
@Transactional(readOnly=true)
public class UserServiceImpl extends UserServiceSupport{
public UserServiceImpl() {
super.setEntityClass(User.class);
}
/**
* 更新个人信息
*/
@Override
@Transactional(readOnly=false)
public void updateUserInfo(User user){
String avatarBase64 = user.getAvatarBase64();
if (StringUtils.isNotBlank(avatarBase64)){
if ("EMPTY".equals(avatarBase64)){
user.setAvatar(StringUtils.EMPTY);
}else{
String imageUrl = "avatar/"+user.getCorpCode()+"/"
+user.getUserType()+"/"+user.getUserCode()
+"."+FileUtils.getFileExtensionByImageBase64(avatarBase64);
String fileName = Global.getUserfilesBaseDir(imageUrl);
FileUtils.writeToFileByImageBase64(fileName, avatarBase64);
user.setAvatar(Global.USERFILES_BASE_URL + imageUrl);
}
}
super.updateUserInfo(user);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
举例 2:
@Service
@Transactional(readOnly=true)
public class RoleServiceImpl extends RoleServiceSupport {
public RoleServiceImpl() {
super.setEntityClass(Role.class);
}
@Override
public void addDataScopeFilter(Role entity) {
// 以下是角色列表过滤条件,覆写该方法可重写过滤条件
User currentUser = role.getCurrentUser(); // v5.3.0 之前版本
User currentUser = role.currentUser(); // v5.3.0+ 及之后版本
SqlMap sqlMap = role.getSqlMap(); // v5.3.0 之前版本
SqlMap sqlMap = role.sqlMap(); // v5.3.0+ 及之后版本
sqlMap.getWhere().disableAutoAddCorpCodeWhere();
sqlMap.getDataScope().addFilter("dsf", "Role", "a.role_code", ctrlPermi);
sqlMap.getDataScope().addFilter("dsf", "a.is_sys = '"+Global.YES
+"' OR a.create_by = '"+currentUser.getUserCode()+"'");
if (currentUser.isAdmin()){
sqlMap.getDataScope().addFilter("dsf", "a.is_sys = '"+Global.NO
+"' AND a.corp_code = '"+ CorpUtils.getCurrentCorpCode()+"'");
}
}
@Override
public List<Role> findList(Role role) {
return super.findList(role);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31