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

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. [代码审计基础 15]phpmcs_v9.6 rce

    PHP:5.4.5 设置调试:https://blog.csdn.net/m0_46641521/article/details/120107786 版本:phpcms_v9.6.0_UTF8 0x0 ...

  2. ctfhubweb-web前置技能-http协议

    HTTP协议 请求方式 看了一下嘤语提示,原来是要使用CTFHUB请求方式 burp抓包,修改一下就好 302 跳转 brup抓包,发送到repeater中,send一下就出来了 cookie 提示需 ...

  3. Cesium渲染模块之概述

    1. 引言 Cesium是一款三维地球和地图可视化开源JavaScript库,使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,基于Apache2.0许可的开源程序,可以免费用于商业和非商业 ...

  4. AI 能多强「GitHub 热点速览」

    不知道 AI 在你那边是什么样的具象,在我这就是各种搞图:从给线稿图上色,到直接给你生成一张小色图,AI 最近是真出风头,本周热点速览也收录了 2 个 AI 项目,也和图像有关.还有一个和 AI 相关 ...

  5. 几种排序(c#实现)

    代码: int[] arr = { 2, 3, 4, 6, 1, 5, 4 }; // 冒泡排序:把最小的往前冒 O(n2) //int temp1; //for (int i = 0; i < ...

  6. pat乙级 1020 月饼

    #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> ...

  7. 【转载】python解决文本乱码问题及文本二进制读取后的处理

    转自:https://blog.csdn.net/u011316258/article/details/50450079 python解决文本乱码问题及文本二进制读取后的处理 吲哚乙酸 当文本中含有很 ...

  8. No.1.8

    定位 网页常见布局 标准流(块级元素独占一行-->垂直布局,行内元素/行内块元素一行显示多个-->水平布局) 浮动(可以让原本垂直布局的块级元素变成水平布局) 定位(可以让元素自由的摆放在 ...

  9. $forceUpdate和this.$set('userInfo',name,'小红');

    在Vue官方文档中指出,$forceUpdate具有强制刷新的作用. 那在vue框架中,如果data中有一个变量:age,修改他,页面会自动更新. 但如果data中的变量为数组或对象,我们直接去给某个 ...

  10. 码云或github的"免费服务器"

    目录 一. 码云及工具介绍 二. 操作步骤 (1) 创建vue-cli项目 (2) 码云创建仓库 (3) 修改并提交项目到码云仓库 (4) 运行项目 (5) 注意点 三. 尾声 对于学生党来说,买个服 ...