! 重要: 遐(瞎)想的思路, 希望各位多多建议

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());
}

看起来除了有点乱之外, 功能基本是正常的

但是写起来是不是太麻烦了, 这么多判断, 搞不好明天还让我写这玩意, 乏味

构思

问题有了, 想想思路

  1. query对象的字段名, 除了between查询外, 均一致
  2. 如果query的范围查询, 都以Min/Max结尾来表示最大值/最小值, 那么也有迹可循
  3. 查询条件只有query对象中的属性, 如果query对象中添加一些注解, 然后检索时解析这些注解, 好像是可行的
  4. 所以只需要写一个工具类, 来解析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);
}

实现

  1. 定义查询类型

    列举一下项目中用的到的检索类型, 篇幅有限, 删掉了注释, 注释版可见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
    }
  2. 定义注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    public @interface MbpQuery {
    QueryTypeEnum type();
    }
  3. 定义工具类

    @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());
    }
    }
  4. 卡住了

    这里如果使用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日志:

确实是我们想要的

总结

多多少少还是有些弊端的, 如:

  1. 相较于原来, 缺少了实体类与检索pojo类的关联, 导致删字段时不方便查找引用
  2. 暂时还没遇到, 可能性能会有点问题, 我认为影响很小, 尤其是后台管理项目
  3. 我不太擅长写文章, 没写清楚思路

基于Mybatis Plus的一种查询条件构建方案的更多相关文章

  1. Form Builder的三种查询方法构建

    1.使用DEFAULT_WHERE: DECLARE   V_DEFAULT_WHERE VARCHAR2(32767);  V_WHERE         VARCHAR2(32767); BEGI ...

  2. find、findIndex、indexOf、lastIndex、includes 数组五种查询条件方法介绍

    find() 方法返回数组中满足提供的测试函数的第一个元素的值. 语法: arr.find(callback[, thisArg]) findIndex()方法返回数组中满足提供的测试函数的第一个元素 ...

  3. Java开发学习(四十四)----MyBatisPlus查询语句之查询条件

    1.查询条件 前面我们只使用了lt()和gt(),除了这两个方法外,MybatisPlus还封装了很多条件对应的方法. MybatisPlus的查询条件有很多: 范围匹配(> . = .betw ...

  4. MongoDB的批量查询条件进行批量更新数据

    今天遇到这样一个场景:在Java中批量更新MongoDB数据,不过每次更新的条件有不一样,那如何有效地进行更新操作呢? 刚开始的时候,我是想到循环批量更新操作,即每一种查询条件进行一次批量更新过程,这 ...

  5. Azure 基础:自定义 Table storage 查询条件

    本文是在 <Azure 基础:Table storage> 一文的基础上介绍如何自定义 Azure Table storage 的查询过滤条件.如果您还不太清楚 Azure Table s ...

  6. MyBatis Generator Example.Criteria 查询条件复制

    背景: 我们在开发中使用MyBatis Generator生成的 XxxExample查询时,咋添加 or 查询时候,可能两个 Example.Criteria 对象的条件存在交集,即多个查询条件是相 ...

  7. SQL Server 存储过程中处理多个查询条件的几种常见写法分析,我们该用那种写法

    本文出处: http://www.cnblogs.com/wy123/p/5958047.html 最近发现还有不少做开发的小伙伴,在写存储过程的时候,在参考已有的不同的写法时,往往很迷茫,不知道各种 ...

  8. Java ->在mybatis和PostgreSQL Json字段作为查询条件的解决方案

    Date:2019-11-15 读前思考: 你没想到解决办法? PostgreSQL 数据库本身就支持还是另有解决办法? 说明:首先这次数据库使用到Json数据类型的原因,这次因为我们在做了一个app ...

  9. 【mysql】 mybatis实现 主从表 left join 1:n 一对多 分页查询 主表从表都有查询条件 【mybatis】count 统计+JSON查询

    mybatis实现 主从表 left join  1:n 一对多 分页查询   主表从表都有查询条件+count 需求: ======================================= ...

  10. 用easyui实现查询条件的后端传递并自动刷新表格的两种方法

    用easyui实现查询条件的后端传递并自动刷新表格的两种方法 搜索框如下: 通过datagrid的load方法直接传递参数并自动刷新表格 通过ajax的post函数传递参数并通过loadData方法将 ...

随机推荐

  1. html+css+js实现一个简易日历

    0.效果预览 只实现了日历最基础的功能,当前日期红色显示,可通过上方的左右按钮查看上一月或下一月的日期. 1.HTML部分 1 <body> 2 <div id="cldF ...

  2. 简述cpu、gpu、fpga和asic四种人工智能芯片的性能

    https://fastonetech.com/newszblog/post/25570.html 简述cpu.gpu.fpga和asic四种人工智能芯片的性能FPGA(Field Programma ...

  3. freeRTOS移植成功

    今天来学习如何移植freeRTOS 也算是走了很多的坑,总算是把系统跑起来了 相关的教程网上也有比较详细的,本文主要说说自己踩的坑 一些汇编文件报错的问题 这个问题的原因是因为网上大部分的移植说明都是 ...

  4. javaWeb学习一

    web开发(web就是网页): 静态web html.css 提供给所有人看到数据不会改变 动态web 提供给所有人看到数据会改变,不同用户.不同时间和地点都会不同 技术栈:Servlet/JSP,A ...

  5. djangoDRF查询

    DRF全部查询基础示例 from django.db.models import Q, F, Sum, Avg, Count, Min, Max from rest_framework.respons ...

  6. leetcode 31. 下一个排列 【时间击败 100%】 【内存击败 92.17%】

    对于该题,我本来兴致勃勃地想到了两个优化,但从提交结果来看根本看不出来有什么区别,但我发4我说的都是真的 -3- 1 public void nextPermutation(int[] nums) { ...

  7. Java实现简单薪水计算器相关操作代码

    /** * 薪水计算器 * 1.通过键盘输入用户的月薪,每年是几个薪水 * 2.输出用户年薪 * 3.输出一行字"如果年薪超过10万,恭喜你超越了90%的国人:如果年薪超过了20万,恭喜你超 ...

  8. csp201503-2(数字排序)

    问题描述 给定n个整数,请统计出每个整数出现的次数,按出现次数从多到少的顺序输出. 输入格式 输入的第一行包含一个整数n,表示给定数字的个数. 第二行包含n个整数,相邻的整数之间用一个空格分隔,表示所 ...

  9. Vue 实现点击空白处隐藏某节点

    手动监听判断 <template> <div> <span ref="projectButton"> <el-popover v-mode ...

  10. vue2的反向代理

    // 在vue.config.js中的配置项配置 devServer: { proxy: { // 当我们的本地的请求 有/api的时候,就会代理我们的请求地址向另外一个服务器发出请求 '/api': ...