! 重要: 遐(瞎)想的思路, 希望各位多多建议
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<>();
// 处理非between字段
fieldValueMap.forEach((field, value) -> {
MbpQuery annotation = field.getAnnotation(MbpQuery.class);
// noinspection AlibabaSwitchStatement
switch (annotation.type()) {
case EQ -> result.eq(mask(field), value);
case NE -> result.ne(mask(field), value);
case GT -> result.gt(mask(field), value);
case GE -> result.ge(mask(field), value);
case LT -> result.lt(mask(field), value);
case LE -> result.le(mask(field), value);
case LIKE -> result.like(mask(field), value);
case NOT_LIKE -> result.notLike(mask(field), value);
case LIKE_LEFT -> result.likeLeft(mask(field), value);
case LIKE_RIGHT -> result.likeRight(mask(field), value);
case NOT_LIKE_LEFT -> result.notLikeLeft(mask(field), value);
case NOT_LIKE_RIGHT -> result.notLikeRight(mask(field), value);
case IN -> result.in(mask(field), value);
case NOT_IN -> result.notIn(mask(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(mask(fieldName), value, maxValue);
case NOT_BETWEEN -> result.notBetween(mask(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());
}
private static <T> SFunctionMask<T> mask(String fieldName) {
return new SFunctionMask<>(fieldName);
}
private static <T> SFunctionMask<T> mask(Field field) {
return mask(field.getName());
}
}
跑一下试试
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类的关联, 导致删字段时不方便查找引用
- 暂时还没遇到, 可能性能会有点问题, 我认为影响很小, 尤其是后台管理项目
- 我不太擅长写文章, 没写清楚思路