前言
上一篇我们完成了对服务器存储空间的优化,本篇开始,进入后台管理功能开发阶段。本篇重点带领大家实现 MyBatis 的分页查询和自定义查询功能,同时对 MyBatis 通用 Mapper 中现存的一些问题给予修正。
功能分析
通常,后台管理系统中常用到分页查询及模糊查询。要求有接口权限限制,避免非管理员用户访问到仅管理员可以访问的接口。本专栏前面花费了 6 篇内容带领大家搭建起 Keller Notes 项目的服务端架构,现在,我们结合项目中现有的服务端架构思考以下几个问题:
- 现有架构是否能准确区分用户身份?
- 现有架构是够能实现接口权限的控制?
- 当前的通用 Mapper 是否能支持分页查询?
- 当前的通用 Mapper 是否能支持模糊查询及自定义查询?
- 当前的通用 Mapper 在设计上是否存在缺陷?
用户身份判断:
- 这设计 JWT 时,存入了用户类型。
- 可以在 JWT 验证完毕后,取出用户类型,用来区分普通用户和管理员。
接口权限控制:
- 需要请求者的用户 ID 的接口中,参数名都固定为 kellerUserId,且该参数无法通过请求中携带的查询参数、Body 内容指定,只解析 JWT 中存储的用户 ID。
- 按照相同的思路,只需要在仅管理员可访问的接口中,将用户 ID 的参数名固定为 kellerAdminId 即可,并限制为:只能从 JWT 中取出管理员的用户 ID 作为该参数。
- 当前,用户登录后生成的 JWT 使用 localStorage 永久保存在用户本地。而管理员的 JWT 所需的安全等级更高,需要保存在 sessionStorage 中,仅会话打开状态生效,关闭会话后清除。
分页查询:
- 在 BaseEntity.java 中,定义了属性 baseKyleUseAnd、baseKyleUseASC 分别用来指定查询时多条件的连接方式、查询时的排序方式等。
- 按照同样的思路,只需要增加 page、rowNum 等参数,即可使其支持分页查询。
模糊查询及自定义查询:
- 通用 Mapper 当前的设计已经支持了自定义查询,但仅限于部分字段的等值查询,暂时不支持模糊查询和范围查询。
- 结合 MyBatis 的灵活性,可以通过指定查询条件的方式支持各种形式的自定义查询模糊查询。只需要将自定义的查询条件书写在 Mapper 中,同时在 BaseEntity.java 中添加属性,接受自定义的查询条件即可。
当前通用 Mapper 存在的缺陷:
- 在设计通用 Mapper 时,使用 @IndexAttribute 注解标注的字段被指定为索引,且在查询时,该字段有值时,会被作为查询条件。
- 这就引出一个问题:是否经常作为查询条件的字段都要指定为索引?答案显然不是的。如:用户类型、用户状态等字段,经常作为查询条件,但其值重复性大,不适合被指定为索引。因此,将索引字段和查询条件分开设计,索引字段和查询字段若有值,就作为查询条件。
代码实现
查询条件与索引分开
设计方案为:
- 删除 @IndexAttribute 注解,在 @FieldAttribute 注解中添加 isIndex 属性用来标示该字段为索引字段,添加 isCondition 属性用来标示该字段为查询字段。
- 检查每个 Entity,分析每个 @IndexAttribute 注解标注的字段:
- 适合设置为索引的字段,将其 @FieldAttribute 注解中的 isIndex 设置为 true,将该字段创建为索引;
- 不适合设置为索引的字段,将其 @FieldAttribute 注解中的 isCondition 设置为 true,可作为查询条件使用。
- 在创建索引时,将判断条件为带有 @IndexAttribute 注解的字段,修改为 @FieldAttribute 注解中 isIndex 为 true 的字段。
- 在创建查询条件时,将判断条件为 @IndexAttribute 注解标注的有值字段,修改为 @FieldAttribute 注解中 isIndex 为 true 或 isCondition 为 true 的字段。
具体实现如下。
在 FieldAttribute.java 中添加 isIndex 属性和 isCondition 属性:
/**
* 是否是索引字段,如果是索引字段,在建表时会以该字段创建索引,在查询时,字段有值会作为查询条件使用
* @return
*/
boolean isIndex() default false;
/**
* 是否要作为查询条件,如果作为查询条件,该字段若有值,会作为查询条件使用
* @return
*/
boolean isCondition() default false;
在 Entity 中修改,以 UserInfo.java 举例:type 字段常用来做查询条件,却不适合创建索引,因此在其 @FieldAttribute 注解中添加 isCondition = true
;email 字段常用来做查询条件,也适合创建索引,因此在其 @FieldAttribute 注解中添加 isIndex = true
:
@FieldAttribute(value = "用户类型",notNull = true,isCondition = true)
private Integer type;
@FieldAttribute(value = "密码",length = 200, isDetailed = true)
private String password;
@FieldAttribute(value = "邮箱",notNull = true,length = 200,isIndex = true)
private String email;
@FieldAttribute
private Date createTime = new Date();
@FieldAttribute(value = "用户账号状态",isCondition = true)
private Integer status ;
修改创建索引的条件,在 SqlFieldReader.java 的 getCreateIndexSql 方法中做如下修改:
public static <T extends BaseEntity> String getCreateIndexSql(T entity){
... ...
// 带有 @FieldAttribute 注解,且 isIndex 为 true 时才创建索引
FieldAttribute fieldAttribute = field.getAnnotation(FieldAttribute.class);
if(fieldAttribute != null && fieldAttribute.isIndex()){
.... ...
}
}
return builder.toString();
}
修改拼接的查询条件,在 SqlFieldReader.java 的 getConditionSuffix 方法中做如下修改:
public static <T extends BaseEntity> String getConditionSuffix(T entity){
... ...
StringBuilder builder = new StringBuilder();
builder.append(" WHERE ");
try {
FieldAttribute fieldAttribute;
for(Field field:fields){
fieldAttribute = field.getAnnotation(FieldAttribute.class);
if(fieldAttribute == null){
continue;
}
//索引字段或设置为查询条件的字段中,有值的都作为查询条件
if(fieldAttribute.isIndex() || fieldAttribute.isCondition()){
if(SqlFieldReader.hasValue(entity,field.getName())){
builder.append(field.getName())
.append(" = #{").append(field.getName()).append("} ")
.append(condition).append(" ");
}
}
}
... ...
}
分页查询
分页查询的设计方案为:
- 在 BaseEntity.java 中添加 baseKylePageSize、 baseKyleCurrentPage 字段,分别表示页面大小和当前要查询的页码;添加 baseKyleStartRows 字段,用来表示分页查询的起始行号
- 每次设置 baseKylePageSize、baseKyleCurrentPage 字段时,都要重新计算下 baseKyleStartRows 字段的值
- 在 BaseSelectProvider.java 中添加方法 selectPageList 用于支持分页查询
具体实现如下。
添加分页查询必要的参数,在 BaseEntity.java 中添加相应属性:
/**
* 页面大小
*/
private int baseKylePageSize = 10;
/**
* 要查询的页码
*/
private int baseKyleCurrentPage = 1;
/**
* 根据页面大小和要查询的页码计算出的起始行号
*/
private int baseKyleStartRows ;
... ...
// 每次设置是页面大小时,重新计算起始行号
public void setBaseKylePageSize(int baseKylePageSize) {
this.baseKylePageSize = baseKylePageSize;
this.baseKyleStartRows = this.baseKylePageSize * (this.baseKyleCurrentPage - 1);
}
// 每次设置当前页码时,重新计算起始行号
public void setBaseKyleCurrentPage(int baseKyleCurrentPage) {
this.baseKyleStartRows = this.baseKylePageSize * (this.baseKyleCurrentPage - 1);
this.baseKyleCurrentPage = baseKyleCurrentPage;
}
因为 MySQL 中分页查询的语法为 limit offset,rows
,也就是说需要起始行号、查询的行数这两个参数;而我们常用的表述方式为:每页显示 10 条数据、查询第 5 页,用到的参数为:页面大小、当前页码。
因此,在做分页查询时,只需要设置 baseKylePageSize、baseKyleCurrentPage 这两个参数,而实际拼接查询语句时,使用的是 baseKyleStartRows、baseKylePageSize 这两个参数。
实现分页查询方法,在 BaseSelectProvider.java 中实现分页查询方法,该方法基于 selectList,因此支持 selectList 支持的各种查询条件:
public static <T extends BaseEntity> String selectPageList(T entity){
String sql = selectList(entity) + " LIMIT #{baseKyleStartRows},#{baseKylePageSize}";
Console.info("selectPageList",sql,entity);
return sql;
}
至此,就完成了对于分页查询的支持,在项目中只需要按照如下格式使用即可:
// 构建 Entity 对象(必须继承 BaseEntity 类)
UserInfo userInfo = new UserInfo();
// 设置要查询的页码(当前页码)
userInfo.setBaseKyleCurrentPage(page);
// 设置每页要显示的记录行数(页面大小)
userInfo.setBaseKylePageSize(pageSize);
// 执行分页查询并获得查询结果
List<UserInfo> list = userMapper.baseSelectPageList(userInfo);
支持自定义查询条件
这里提到的自定义查询主要是针对非等值查询而言的,因为当前通用 Mapper 的设计已经支持了多字段自由组合的自定义等值查询。自定义查询的设计方案如下:
- 在 BaseEntity.java 中添加参数,用于接受自定义的查询条件
- 在 Mapper 中书写自定义查询条件
- 在使用时,指定自定义查询条件
- 在生成 SQL 语句中的查询条件时,判断是否设置了自定义查询条件
- 如果设置了查询条件,只取设置的查询条件
- 否则,按照原来的规则构建查询条件
具体使用如下。
设置参数接收自定义查询条件,在 BaseEntity.java 中添加参数 baseKyleCustomCondition,用于接收自定义查询条件:
/**
* 自定义查询条件,指定自定义查询条件后,执行查询语句时,只会选择自定义的查询条件执行,忽略其他条件
*/
private String baseKyleCustomCondition = null;
以用户名模糊查询为例,在 UserMapper.java 中书写查询条件,只用写 WHERE 后面的语句即可,格式与 MyBatis 中 SQL 的格式保持一致:
@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {
/**
* 自定义查询条件:按邮箱模糊查询
* 按照 MyBatis 的 SQL 格式书写
* 该条件通过baseEntity.setBaseKyleCustomCondition设置
*/
String whereEmailLike = "email LIKE CONCAT('%',#{email},'%') ";
}
以用户名模糊查询为例,使用时指定自定义查询条件:
UserInfo userInfo = new UserInfo();
// 因为自定义查询条件中是按照 emial 进行模糊匹配的,因此要设置 email 参数
userInfo.setEmail(email);
// 指定自定义查询条件为按照邮箱模糊匹配
userInfo.setBaseKyleCustomCondition(UserMapper.whereEmailLike);
// 执行自定义查询并返回查询结果
List<UserInfo> list = userMapper.baseSelectList(userInfo);
在 SqlFieldReader.java 中 getConditionSuffix 方法做如下修改:
public static <T extends BaseEntity> String getConditionSuffix(T entity){
//如果设置了自定义的查询条件,直接返回自定义查询条件,不再判断字段值
if(entity.getBaseKyleCustomCondition() != null){
return " WHERE " + entity.getBaseKyleCustomCondition();
}
... ...
}
至此,通用 Mapper 支持了自定义查询条件及分页查询。
效果测试
我们以用户管理中常用的“用户名模糊查询”为例,测试通用 Mapper 对自定义查询条件和分页查询的支持,同时该功能也是下一篇要用到的。
新建 AdminController.java,用于处理管理员相关功能的请求,添加获取用户列表的接口:
@RestController
@RequestMapping("/admin")
public class AdminController {
@Resource
private UserService userService;
@GetMapping("/userList")
public ResponseEntity getUserList(Integer kellerAdminId,Integer page,Integer size,String email){
if(StringUtils.isEmpty(kellerAdminId,page,size)){
return Response.badRequest();
}
return Response.ok(userService.getUserList(page,size,email));
}
}
在 UserService.java 中添加方法,用于获取用户列表:判断传入参数中是否有 email 参数,如果有,就按照邮箱名模糊查询;否则分页查询所有用户列表。
public ResultData getUserList(int page,int pageSize,String email){
UserInfo userInfo = new UserInfo();
userInfo.setBaseKyleCurrentPage(page);
userInfo.setBaseKylePageSize(pageSize);
userInfo.setBaseKyleDetailed(false);
//如果传入了email参数,则执行模糊查询
if(StringUtils.noEmpty(email)){
userInfo.setEmail(email);
// 设置自定义查询条件
userInfo.setBaseKyleCustomCondition(UserMapper.whereEmailLike);
}
List<UserInfo> list = userMapper.baseSelectPageList(userInfo);
return ResultData.success(list);
}
请求头:
请求参数及返回结果:
源码地址
本篇完整的源码地址:
小结
本篇实现分页查询与自定义条件查询,为后台管理功能中常用到的模糊查询及范围查询做准备,并修正现有通用 Mapper 使用中存在的问题。