基于Mybatis Plus的一种查询条件构建方案
! 重要: 遐(瞎)想的思路, 希望各位多多建议
record为jdk17写法, 使用class也不会有问题
背景
身为资深程序员, 上班最重要的事当然是增删改查(bushi).
比如今天, 组长甩给了我一个表, 告诉我根据学号/性别/生日范围/受表扬次数写个接口.
create table test_student(
id bigint primary key comment 'id',
stu_num varchar(20) not null comment '学号',
sex varchar(10) not null comment '性别, 字典: sex',
birthday date not null comment '生日',
star_count int not null comment '受表扬次数'
) comment '测试用_学生信息';
用的mybatis plus, 有了如下代码, 于是我今天的工作任务就完成了
// 检索对象
/**
* 学生检索
*
* @param stuNum 学号
* @param sex 性别
* @param birthdayMin 最小生日
* @param birthdayMax 最大生日
* @param starCount 受表扬次数
*/
public record TestStudentQuery(
String stuNum,
String sex,
LocalDate birthdayMin,
LocalDate birthdayMax,
Integer starCount
) {
}
// service search方法
@Override
public Page<TestStudent> search(TestStudentQuery query) {
return this.lambdaQuery()
// 学号不为空, 则模糊查询
.like(StringUtils.isNotEmpty(query.stuNum()), TestStudent::getStuNum, query.stuNum())
// 性别不为空则全等查询
.eq(StringUtils.isNotEmpty(query.sex()), TestStudent::getSex, query.sex())
// 生日最小/最大时间均不为空则进行范围查询
.between(Objects.nonNull(query.birthdayMax()) && Objects.nonNull(query.birthdayMin()),
TestStudent::getBirthday, query.birthdayMin(), query.birthdayMax())
// 受表扬次数不为空则全等查询
.eq(Objects.nonNull(query.starCount()), TestStudent::getStarCount, query.starCount())
// 分页
.page(Paging.startPage());
}
看起来除了有点乱之外, 功能基本是正常的
但是写起来是不是太麻烦了, 这么多判断, 搞不好明天还让我写这玩意, 乏味
构思
问题有了, 想想思路
- query对象的字段名, 除了between查询外, 均一致
- 如果query的范围查询, 都以Min/Max结尾来表示最大值/最小值, 那么也有迹可循
- 查询条件只有query对象中的属性, 如果query对象中添加一些注解, 然后检索时解析这些注解, 好像是可行的
- 所以只需要写一个工具类, 来解析query中的注解就行了呀
基于上面的想法, 把我想要的写法写了出来:
query对象:
public record TestStudentQuery(
@MbpQuery(type = QueryTypeEnum.LIKE)
String stuNum,
@MbpQuery(type = QueryTypeEnum.EQ)
String sex,
@MbpQuery(type = QueryTypeEnum.BETWEEN)
LocalDate birthdayMin,
@MbpQuery(type = QueryTypeEnum.BETWEEN)
LocalDate birthdayMax,
@MbpQuery(type = QueryTypeEnum.EQ)
Integer starCount
) {
}
service中写法:
@Override
public Page<TestStudent> anotherSearch(TestStudentQuery query) {
// 用法1: 自动解析query对象, 整理为mybatis plus理解的代码, 只构建查询条件, 不进行自动分页查询, 便于后续查询
MbpUtil.buildSearch(this, query)
// 用法2: 相对于buildSearch, 并自动解析分页
return MbpUtil.page(this, query);
}
实现
定义查询类型
列举一下项目中用的到的检索类型, 篇幅有限, 删掉了注释, 注释版可见github
public enum QueryTypeEnum {
EQ,
NE,
GT,
GE,
LT,
LE,
BETWEEN,
NOT_BETWEEN,
LIKE,
NOT_LIKE,
LIKE_LEFT,
LIKE_RIGHT,
NOT_LIKE_LEFT,
NOT_LIKE_RIGHT,
IN,
NOT_IN
}
定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface MbpQuery {
QueryTypeEnum type();
}
定义工具类
@SuppressWarnings("AlibabaConstantFieldShouldBeUpperCase")
public class MbpUtil1 {
/**
* between字段区间开始标识
*/
private static final String betweenMin = "Min";
/**
* between字段区间结尾标识
*/
private static final String betweenMax = "Max"; /**
* 用于快速构建检索条件
*
* @param service service对象
* @param query 检索条件, query对象
* @param <M> mapper interface
* @param <T> 实体类
* @return 构建完成的检索条件
*/
public static <M extends BaseMapper<T>, T> LambdaQueryChainWrapper<T> buildSearch(ServiceImpl<M, T> service, Object query) {
QueryChainWrapper<T> result = service.query();
// switch (query 的字段){
// 找到各种注解, 根据不同的注解去调用不同的方法并判断非空
// }
// 这里返回lambda/还是非lambda更合适?
return result;
} /**
* 快速构建检索条件并分页
* @param service service对象
* @param query 检索条件, query对象
* @param <M> mapper interface
* @param <T> 实体类
* @return 分页结果
*/
public static <M extends BaseMapper<T>, T> Page<T> page(ServiceImpl<M, T> service, Object query) {
return buildSearch(service, query).page(Paging.startPage());
}
}
卡住了
这里如果使用mybatis plus的QueryChain好像没啥事, 但是这里需要返回半成品的查询对象, 那么外面继续添加检索条件时, 就无法使用lambda方式了
字符串里面写东西不好, 不好排查, 没有代码提示, 不能查找引用
那么还是要使用lambda方式构建查询
解决问题
那么非要使用lambda的话, 第一个参数怎么传递呢
service中调用this.lambdaQuery().eq方法, 字段名使用SFunction类型接收, 这个参数必须是lambda方式, 否则无法获取方法名, 也就无法获取变量名
让我们来看一眼它是怎么获取方法名的吧, 最终我找到了这个工具方法: com.baomidou.mybatisplus.core.toolkit.LambdaUtils#extract
/**
* 该缓存可能会在任意不定的时间被清除
*
* @param func 需要解析的 lambda 对象
* @param <T> 类型,被调用的 Function 对象的目标类型
* @return 返回解析后的结果
*/
public static <T> LambdaMeta extract(SFunction<T, ?> func) {
// 1. IDEA 调试模式下 lambda 表达式是一个代理
if (func instanceof Proxy) {
return new IdeaProxyLambdaMeta((Proxy) func);
}
// 2. 反射读取
try {
Method method = func.getClass().getDeclaredMethod("writeReplace");
return new ReflectLambdaMeta((SerializedLambda) ReflectionKit.setAccessible(method).invoke(func));
} catch (Throwable e) {
// 3. 反射失败使用序列化的方式读取
return new ShadowLambdaMeta(com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.extract(func));
}
}
最终返回了一个LambdaMeta接口(两个方法, 一个获取实体类名, 一个获取字段名)
查看其注释, 我们要构造一个SFunction的实现类, 其具有writeReplace方法, 返回值是一个SerializedLambda对象
此对象不会是代理, 因为我们无法使用lambda, 也不会反序列化失败, 所以只需要兼容其第二步即可
声明这么一个对象看看效果:
/**
* 构造mbp的lambda表达式, 用以欺骗mbp
*
* @author ly-chn
* @see com.baomidou.mybatisplus.core.toolkit.support.SFunction
*/
@SuppressWarnings("AlibabaClassNamingShouldBeCamel")
@AllArgsConstructor
public class SFunctionMask<T> implements SFunction<T, Object> {
private String fieldName;
@Override
public Object apply(T t) {
return null;
}
@SuppressWarnings("unused")
private SerializedLambda writeReplace() {
return new SerializedLambda(
null,
null,
null,
null,
0,
null,
"get" + fieldName,
null,
"LY" + instantiatedMethodType + ";",
new Object[0]
);
}
}
SFunctionMask的目的很明确, 就是实现SFunction, 且有一个能够返回SerializedLambda对象的writeReplace方法
返回的SerializedLambda只用到了两个内容: 所属类名, 方法名
类名即实体类名, 调用service.getEntityClass()
即可获取, 添加"LY"前缀和分号后缀的原因见com.baomidou.mybatisplus.core.toolkit.support.ReflectLambdaMeta#getInstantiatedClass
;
方法名需要以get/set开头, mybatis plus会自动去掉
那么, 问题解决了
工具类最终实现
篇幅有限, 删掉注释和导包, 完整代码见github
public class MbpUtil {
private static final String betweenMin = "Min";
private static final String betweenMax = "Max";
public static <M extends BaseMapper<T>, T> LambdaQueryChainWrapper<T> buildSearch(ServiceImpl<M, T> service, Object query) {
LambdaQueryChainWrapper<T> result = service.lambdaQuery();
HashMap<Field, Object> fieldValueMap = new HashMap<>();
for (Field field : FieldUtils.getFieldsListWithAnnotation(query.getClass(), MbpQuery.class)) {
try {
Object value = FieldUtils.readField(field, query, true);
if (Objects.isNull(value) || (value instanceof String && StringUtils.isEmpty((String) value))) {
continue;
}
fieldValueMap.put(field, value);
} catch (IllegalAccessException ignored) {
}
}
if (fieldValueMap.isEmpty()) {
return result;
}
HashMap<Field, Object> betweenFieldValueMap = new HashMap<>();
String entityClassName = service.getEntityClass().getName();
Function<String, SFunctionMask<T>> nameMask = fieldName -> new SFunctionMask<>(fieldName, entityClassName);
Function<Field, SFunctionMask<T>> mask = field -> new SFunctionMask<>(field.getName(), entityClassName);
// 处理非between字段
fieldValueMap.forEach((field, value) -> {
MbpQuery annotation = field.getAnnotation(MbpQuery.class);
// noinspection AlibabaSwitchStatement
switch (annotation.type()) {
case EQ -> result.eq(mask.apply(field), value);
case NE -> result.ne(mask.apply(field), value);
case GT -> result.gt(mask.apply(field), value);
case GE -> result.ge(mask.apply(field), value);
case LT -> result.lt(mask.apply(field), value);
case LE -> result.le(mask.apply(field), value);
case LIKE -> result.like(mask.apply(field), value);
case NOT_LIKE -> result.notLike(mask.apply(field), value);
case LIKE_LEFT -> result.likeLeft(mask.apply(field), value);
case LIKE_RIGHT -> result.likeRight(mask.apply(field), value);
case NOT_LIKE_LEFT -> result.notLikeLeft(mask.apply(field), value);
case NOT_LIKE_RIGHT -> result.notLikeRight(mask.apply(field), value);
case IN -> result.in(mask.apply(field), value);
case NOT_IN -> result.notIn(mask.apply(field), value);
case BETWEEN, NOT_BETWEEN -> betweenFieldValueMap.put(field, value);
default -> throw new LyException.Panic("架构支持能力不足");
}
});
// 处理between字段
betweenFieldValueMap.forEach((field, value) -> {
if (field.getName().endsWith(betweenMin)) {
String fieldName = field.getName().substring(0, field.getName().length() - betweenMin.length());
String maxFieldName = fieldName + betweenMax;
Optional<Field> maxFieldOptional = betweenFieldValueMap.keySet().stream()
.filter(it -> it.getName().contentEquals(maxFieldName)).findFirst();
if (maxFieldOptional.isEmpty()) {
return;
}
Field maxField = maxFieldOptional.get();
Object maxValue = betweenFieldValueMap.get(maxField);
MbpQuery annotation = field.getAnnotation(MbpQuery.class);
// noinspection AlibabaSwitchStatement
switch (annotation.type()) {
case BETWEEN -> result.between(nameMask.apply(fieldName), value, maxValue);
case NOT_BETWEEN -> result.notBetween(nameMask.apply(fieldName), value, maxValue);
default -> throw new LyException.Panic("架构支持能力不足");
}
}
});
return result;
}
public static <M extends BaseMapper<T>, T> Page<T> page(ServiceImpl<M, T> service, Object query) {
return buildSearch(service, query).page(Paging.startPage());
}
}
跑一下试试
controller:
@RestController
@RequestMapping("/test-student")
@RequiredArgsConstructor
@Slf4j
public class TestStudentController {
private final TestStudentService service;
@GetMapping("search")
public Page<TestStudent> search(TestStudentQuery query) {
return service.search(query);
}
}
query:
public record TestStudentQuery(
@MbpQuery(type = QueryTypeEnum.LIKE)
String stuNum,
@MbpQuery(type = QueryTypeEnum.EQ)
String sex,
@MbpQuery(type = QueryTypeEnum.BETWEEN)
LocalDate birthdayMin,
@MbpQuery(type = QueryTypeEnum.BETWEEN)
LocalDate birthdayMax,
@MbpQuery(type = QueryTypeEnum.EQ)
Integer starCount
) {
}
service impl:
@Override
public Page<TestStudent> search(TestStudentQuery query) {
return MbpUtil.page(this, query);
}
test.http:
### 全部
GET http://localhost:8937/test-student/search?stuNum=1&sex=man&birthdayMin=2000-01-01&birthdayMax=2022-01-01&starCount=10
### between需要校验最大最小不能为空
GET http://localhost:8937/test-student/search?birthdayMax=2022-01-01
查看一下SQL日志:
确实是我们想要的
总结
多多少少还是有些弊端的, 如:
- 相较于原来, 缺少了实体类与检索pojo类的关联, 导致删字段时不方便查找引用
- 暂时还没遇到, 可能性能会有点问题, 我认为影响很小, 尤其是后台管理项目
- 我不太擅长写文章, 没写清楚思路
基于Mybatis Plus的一种查询条件构建方案的更多相关文章
- Form Builder的三种查询方法构建
1.使用DEFAULT_WHERE: DECLARE V_DEFAULT_WHERE VARCHAR2(32767); V_WHERE VARCHAR2(32767); BEGI ...
- find、findIndex、indexOf、lastIndex、includes 数组五种查询条件方法介绍
find() 方法返回数组中满足提供的测试函数的第一个元素的值. 语法: arr.find(callback[, thisArg]) findIndex()方法返回数组中满足提供的测试函数的第一个元素 ...
- Java开发学习(四十四)----MyBatisPlus查询语句之查询条件
1.查询条件 前面我们只使用了lt()和gt(),除了这两个方法外,MybatisPlus还封装了很多条件对应的方法. MybatisPlus的查询条件有很多: 范围匹配(> . = .betw ...
- MongoDB的批量查询条件进行批量更新数据
今天遇到这样一个场景:在Java中批量更新MongoDB数据,不过每次更新的条件有不一样,那如何有效地进行更新操作呢? 刚开始的时候,我是想到循环批量更新操作,即每一种查询条件进行一次批量更新过程,这 ...
- Azure 基础:自定义 Table storage 查询条件
本文是在 <Azure 基础:Table storage> 一文的基础上介绍如何自定义 Azure Table storage 的查询过滤条件.如果您还不太清楚 Azure Table s ...
- MyBatis Generator Example.Criteria 查询条件复制
背景: 我们在开发中使用MyBatis Generator生成的 XxxExample查询时,咋添加 or 查询时候,可能两个 Example.Criteria 对象的条件存在交集,即多个查询条件是相 ...
- SQL Server 存储过程中处理多个查询条件的几种常见写法分析,我们该用那种写法
本文出处: http://www.cnblogs.com/wy123/p/5958047.html 最近发现还有不少做开发的小伙伴,在写存储过程的时候,在参考已有的不同的写法时,往往很迷茫,不知道各种 ...
- Java ->在mybatis和PostgreSQL Json字段作为查询条件的解决方案
Date:2019-11-15 读前思考: 你没想到解决办法? PostgreSQL 数据库本身就支持还是另有解决办法? 说明:首先这次数据库使用到Json数据类型的原因,这次因为我们在做了一个app ...
- 【mysql】 mybatis实现 主从表 left join 1:n 一对多 分页查询 主表从表都有查询条件 【mybatis】count 统计+JSON查询
mybatis实现 主从表 left join 1:n 一对多 分页查询 主表从表都有查询条件+count 需求: ======================================= ...
- 用easyui实现查询条件的后端传递并自动刷新表格的两种方法
用easyui实现查询条件的后端传递并自动刷新表格的两种方法 搜索框如下: 通过datagrid的load方法直接传递参数并自动刷新表格 通过ajax的post函数传递参数并通过loadData方法将 ...
随机推荐
- [代码审计基础 15]phpmcs_v9.6 rce
PHP:5.4.5 设置调试:https://blog.csdn.net/m0_46641521/article/details/120107786 版本:phpcms_v9.6.0_UTF8 0x0 ...
- ctfhubweb-web前置技能-http协议
HTTP协议 请求方式 看了一下嘤语提示,原来是要使用CTFHUB请求方式 burp抓包,修改一下就好 302 跳转 brup抓包,发送到repeater中,send一下就出来了 cookie 提示需 ...
- Cesium渲染模块之概述
1. 引言 Cesium是一款三维地球和地图可视化开源JavaScript库,使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,基于Apache2.0许可的开源程序,可以免费用于商业和非商业 ...
- AI 能多强「GitHub 热点速览」
不知道 AI 在你那边是什么样的具象,在我这就是各种搞图:从给线稿图上色,到直接给你生成一张小色图,AI 最近是真出风头,本周热点速览也收录了 2 个 AI 项目,也和图像有关.还有一个和 AI 相关 ...
- 几种排序(c#实现)
代码: int[] arr = { 2, 3, 4, 6, 1, 5, 4 }; // 冒泡排序:把最小的往前冒 O(n2) //int temp1; //for (int i = 0; i < ...
- pat乙级 1020 月饼
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> ...
- 【转载】python解决文本乱码问题及文本二进制读取后的处理
转自:https://blog.csdn.net/u011316258/article/details/50450079 python解决文本乱码问题及文本二进制读取后的处理 吲哚乙酸 当文本中含有很 ...
- No.1.8
定位 网页常见布局 标准流(块级元素独占一行-->垂直布局,行内元素/行内块元素一行显示多个-->水平布局) 浮动(可以让原本垂直布局的块级元素变成水平布局) 定位(可以让元素自由的摆放在 ...
- $forceUpdate和this.$set('userInfo',name,'小红');
在Vue官方文档中指出,$forceUpdate具有强制刷新的作用. 那在vue框架中,如果data中有一个变量:age,修改他,页面会自动更新. 但如果data中的变量为数组或对象,我们直接去给某个 ...
- 码云或github的"免费服务器"
目录 一. 码云及工具介绍 二. 操作步骤 (1) 创建vue-cli项目 (2) 码云创建仓库 (3) 修改并提交项目到码云仓库 (4) 运行项目 (5) 注意点 三. 尾声 对于学生党来说,买个服 ...