如果您在项目使用过mybatisplus,想必有过这样的麻烦,mybatisplus中条件是通过QueryWrapper进行封装,根据不同的业务来确定使用哪种操作符,比如eq,ge,lt等等。比如我们具有相近业务的条件使用同一个条件对象,这样一来就会产生一种非常臃肿的代码结构,并且每增加一个条件,都会牵扯QueryWrapper构造条件的改动,比如下面这样的情形:
只要改动PlFinanceCondition这个条件属性,这个条件的封装就会随之更新,耦合强亦不优雅。为了解决这个问题,我的优化方案为:自定义注解+反射来动态构建QueryWrapper
1、自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Wrapper {
/**
* 条件的关键字
*/
String keyword() default "eq";
/**
* 数据表列
*/
String field() default "";
}
说明:注解条件对象,标明其属性使用哪种操作符(默认是eq)和属性对应的数据表的列名
2、Between操作左右边界对象
@Data
public class Between<T> {
private T bLeft;
private T bRight;
public Between(T bLeft, T bRight) {
this.bLeft = bLeft;
this.bRight = bRight;
}
}
3、条件对象中使用该注解
@Data
@Builder
public class PlFinanceCondition {
********省略部分**********
/**
* 分校编码
* */
@Wrapper(field = "area_code")
private String areaCode;
/**
* 支付方式列表
* */
@Wrapper(keyword = "in",field = "pay_type")
private List<Integer> payTypes;
/**
* 支付方式
* */
@Wrapper(field = "pay_type")
private Integer payType;
/**
* 交易开始时间
* */
@Wrapper(keyword = "ge",field = "trade_time")
private Date tradeTimeStart;
/**
* 交易结束时间
* */
@Wrapper(keyword = "lt",field = "trade_time")
private Date tradeTimeEnd;
/**
* 交易时间 between操作
* */
@Wrapper(keyword = "between",field = "trade_time")
private Between<Date> tradeTimeBetween;
/**
* id,trade_time正序
* */
@Wrapper(keyword = "orderByDesc",field = "id,trade_time")
private Boolean orderByIdAndTradeTimeDesc;
}
说明:在每个属性上添加注解,标识属性使用哪种操作和对应数据表列名称
4、通过反射调用不同的操作方法
QueryWrapper的父类AbstractWrapper实现了4个接口,分别为Compare、Nested、Join和Func,其中我们常用的操作方法(比如eq、ge、gt、lt、in等)都在Compare和Func中,所以我这里就处理这两种接口的方法
4.1、通过枚举类抽象一个接口,两种类型Compare、Func
public enum WrapperInterfaceEnum {
Compare(Wrapper.KeywordsEnum.Compare.getKeyword()){
@Override
public <T> void doHandle(QueryWrapper<T> queryWrapper,Wrapper wrapper,Object entityValue) throws Exception {
Class compareClass = WrapperInterfaceEnum.getClass(this.name(),queryWrapper);
if(Wrapper.KeywordsEnum.Compare_Ext.getKeyword().contains(wrapper.keyword())){
//例:default Children between(R column, Object val1, Object val2)
Method m = compareClass.getDeclaredMethod(wrapper.keyword(),Object.class,Object.class,Object.class);
Between between = (Between)entityValue;
m.invoke(queryWrapper,wrapper.field(),between.getBLeft(),between.getBRight());
}else{
//例:default Children eq(R column, Object val)
Method m = compareClass.getDeclaredMethod(wrapper.keyword(),Object.class,Object.class);
m.invoke(queryWrapper,wrapper.field(),entityValue);
}
}
},
Func(Wrapper.KeywordsEnum.Func.getKeyword()){
@Override
public <T> void doHandle(QueryWrapper<T> queryWrapper,Wrapper wrapper,Object entityValue) throws Exception {
****省略*****
}
}
};
private List<String> keyword;
****省略*****
private static <T> Class getClass(String enumName,QueryWrapper<T> queryWrapper) throws ClassNotFoundException {
Class[] classes = queryWrapper.getClass().getSuperclass().getInterfaces();
Class compareClass = null;
for(Class c : classes){
if(enumName.equals(c.getSimpleName())){
compareClass = Class.forName(c.getName());
break;
}
}
return compareClass;
}
/**
* 每个枚举类型不同的实现逻辑
* @param queryWrapper 封装查询条件
* @param wrapper 注解条件对象的注解类
* @param entityValue 条件中对应值
* */
public abstract <T> void doHandle(QueryWrapper<T> queryWrapper,Wrapper wrapper,Object entityValue) throws Exception;
}
说明:该枚举类定义了一个抽象方法doHandle,每个枚举类型通过不同实现来调用不同的AbstractWrapper实现接口的操作方法。该枚举有两种类型,分别对应Compare和Func接口,每个类型都对应有一个操作符列表
Compare(Lists.newArrayList("eq","ge","gt","le","lt","like","notLike","likeLeft","likeRight","between"))
Func(Lists.newArrayList("in","notIn","groupBy","orderByAsc","orderByDesc"))
这里有个关键点,通过当前实例获取其父类实现的接口类型Class[] classes = queryWrapper.getClass().getSuperclass().getInterfaces();
4.2、封装工具类,来使用该枚举类,构造QueryWrapper查询条件
public class QueryWrapperUtil {
public static <T> void buildQueryWrapper(PlFinanceCondition condition,QueryWrapper<T> queryWrapper){
Field[] fields = condition.getClass().getDeclaredFields();
for(Field field : fields){
field.setAccessible(true);
Wrapper wrapper = field.getAnnotation(Wrapper.class);
if(Objects.nonNull(wrapper)){
try {
Object fieldValue = field.get(condition);
if(Objects.nonNull(fieldValue)){
String keyword = wrapper.keyword();
WrapperInterfaceEnum.getByKeyword(keyword).doHandle(queryWrapper,wrapper,fieldValue);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
说明:根据操作符来确定使用枚举类型,调用对应的doHandle
WrapperInterfaceEnum.getByKeyword(keyword).doHandle(queryWrapper,wrapper,fieldValue)
5、使用方式,通过工具类调用对应的方法
@DataSource(DataSourceTypes.SLAVE_PL_FINANCE)
public List<PlFinance> selectByCondition(PlFinanceCondition condition){
QueryWrapper<PlFinance> queryWrapper = new QueryWrapper<>();
QueryWrapperUtil.buildQueryWrapper(condition,queryWrapper);
Page<PlFinance> page = new Page<>(1, DBConstant.MAX_NUMBER_OF_PIECES);
IPage<PlFinance> ppFinanceIPage = plFinanceMapper.selectPage(page,queryWrapper);
return Lists.newArrayList();
}
6、单元测试,验证结果
@Test
public void testWrapper(){
PlFinanceCondition condition = PlFinanceCondition.builder()
.areaCode("1111")//eq
.payTypes(Lists.newArrayList(103))//in
//.tradeTimeStart(new DateTime("2020-01-01").toDate())//ge
.tradeTimeBetween(new Between<>(new DateTime("2020-01-01").toDate(),new DateTime("2020-06-01").toDate()))//between
.orderByIdAndTradeTimeDesc(true)//orderByDesc
.build();
plFinanceService.selectByCondition(condition);
}
操作符和设置的值对应并生效
总结:通过优化,在使用上我只需关注条件对象本身,而无需关心条件封装的过程,解开条件和封装过程的耦合,降低了因改变条件而没有改变封装过程带来错误的风险。现在要修改条件,只需要在条件对象中增减属性,修改对应注解的操作符即可,nice!!!