一、背景
不知道各位在開發中是否遇到了這樣的問題,就是一般在做一些查詢功能的時候,特別是一些繁瑣的資訊採集類或者erp類系統,前段要傳一大批查詢條件,我們需要在程式碼裡一遍遍的判空,然後在講條件進去組合進去查詢。
假設我們有以下實體類,前端將給我們傳過來這個類,然後我們需要按照這個類的欄位去資料庫裡查詢相關資料。
import com.baomidou.mybatisplus.annotation.*;
import com.linzi.pitpat.base.utils.BeanUtil;
import lombok.Data;
import com.lz.mybatis.plugin.annotations.AS;
import java.math.BigDecimal;
import java.util.Date;import java.util.Date;
@Data
public class Movement implements java.io.Serializable {
private Integer id;
//動作名稱
private String movementName;
//動作英文名稱
private String movementEnName;
//動作難度
private String movementLevel;
//適用性別
private Integer applicableGender;
//動作型別
private Integer movementType;
//鍛鍊部位
private String exercisePart;
//鍛鍊肌肉
private String exerciseMuscle;
//所需器械
private String requireMachine;
//預計消耗熱量
private Integer heatConsumption;
//動作影片
private String video;
//影片時長
private String videoDuration;
//封面
private String cover;
//動作介紹
private String movementIntroduction;
//備註描述
private String remark;
//
private Integer isDelete;
//
private Date gmtModified;
//
private Date gmtCreate;
}
LambdaQueryWrapper<Movement> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(po.getMovementName()), Movement::getMovementName, po.getMovementName())
.like(StringUtils.isNotBlank(po.getMovementEnName()), Movement::getMovementEnName, po.getMovementEnName())
.eq(StringUtils.isNotBlank(po.getMovementLevel()), Movement::getMovementLevel, po.getMovementLevel())
.eq(po.getApplicableGender() != null, Movement::getApplicableGender, po.getApplicableGender())
.eq(po.getMovementType() != null, Movement::getMovementType, po.getMovementType())
.ge(po.getExercisePart() != null, Movement::getExercisePart, po.getExercisePart())
.ge(po.getExerciseMuscle() != null, Movement::getExerciseMuscle, po.getExerciseMuscle())
.ge(po.getRequireMachine() != null, Movement::getRequireMachine, po.getRequireMachine())
.ge(po.getVideo() != null, Movement::getVideo, po.getVideo())
.ge(po.getCover() != null, Movement::getCover, po.getCover())
.like(po.getMovementIntroduction() != null, Movement::getMovementIntroduction, po.getMovementIntroduction())
.like(po.getRemark() != null, Movement::getRemark, po.getRemark())
.le(po.getGmtCreate() != null, Movement::getGmtCreate, po.getGmtCreate())
.orderByDesc(Movement::getGmtCreate);
Page<Movement> page = new Page<>(po.getPageNum(), po.getPageSize());
movementService.page(page, wrapper);
當做一些查詢時,我們要寫這種大量像抹布一樣的程式碼,就姑且稱之為抹布程式碼吧。其實仔細分析這些程式碼和我們要的業務需求,不難發現其實我們要的很簡單,就是根據一些指定的欄位按照指定的比較或者排序方式向資料庫查詢而已,而且大多數場景下這種查詢是單表的。
當我們抽象出我們需要的東西后,事情就變得簡單的。實體類我們有,那能不能有一種快捷的方式去指定查詢實體類上每個欄位的比較方式呢,是等值比較,還是模糊查詢,或者說是大於小於,還有就是跟表裡的那個欄位比較,是否一定要比較呢?
很自然的,我們會想到用註解去實現這一idea,我們可以去寫一個自定義註解,將其放在我們實體類的欄位上,並表明這個欄位需要跟表中的那個欄位對應以及他的比較方式。
二、實現
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target( ElementType.FIELD)
public @interface Compare {
String columnName() default "default";
String compareType();
}
首先,我們擁有了自己的一個標記註解,接下來的工作就是讓這個註解生效了。由於我們專案中使用的orm工具是mybatis-plus,所以下面我給一個基於mybatis-plus實現。
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.linzi.pitpat.data.annotation.Compare;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;
public class MpUtil {
public static <type> QueryWrapper<type> generateWrapper(Type type, Object source){
return generateWrapper(type, source, null);
}
public static <type> QueryWrapper<type> generateWrapper(Type type, Object source, List<String> ignoreFields) {
QueryWrapper<type> wrapper = new QueryWrapper<>();
List<signNode> nodes = getSignNodes(source);
if (CollectionUtils.isNotEmpty(nodes)) {
start(wrapper, nodes, ignoreFields);
}
return wrapper;
}
//啟動組裝wrapper
private static <T> void start(QueryWrapper<T> wrapper, List<signNode> nodes, List<String> ignoreFields) {
for (signNode node : nodes) {
String column = node.getColumn();
Object value = node.getValue();
String symbol = node.getSymbol();
//存在忽略集
if (CollectionUtils.isNotEmpty(ignoreFields)) {
//當前是否是忽略欄位
boolean isIgnoreField = ignoreFields.contains(column) || ignoreFields.contains(StrUtil.toCamelCase(column));
if (isIgnoreField)
continue;
}
try {
Method method = wrapper.getClass().getSuperclass().getDeclaredMethod(symbol, boolean.class, Object.class, Object.class);
method.setAccessible(true);
method.invoke(wrapper, true, column, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static List<signNode> getSignNodes(Object source) {
ArrayList<signNode> list = new ArrayList<>();
Class<?> cls = source.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
// String不應該!=null做判空
Object value = null;
try {
value = field.get(source);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Compare compare = field.getAnnotation(Compare.class);
if (notNull(value) && compare != null) {
String columnName = compare.columnName();
String compareType = compare.compareType();
if ("default".equals(columnName)) {
columnName = StrUtil.toUnderlineCase(field.getName());
}
signNode node = new signNode();
node.setColumn(columnName);
node.setValue(value);
node.setSymbol(compareType);
list.add(node);
}
}
return list;
}
private static boolean notNull(Object value) {
return value instanceof String ? StringUtils.isNotBlank((CharSequence) value) : value != null;
}
原理也很簡單,我們首先透過欄位上的註解拿到我們要去比較的欄位,將其包裝為node,然後看他是否為空,注意,在這裡各個欄位判空的條件不同,比如String型別用的是StringUtils.isNotBlank(),這個由開發者自己把握。然後我們解析獲取這個欄位要去表中對應著那個欄位,根據註解上的columnName獲取,預設情況下代表駝峰轉下劃線。接著,我們再拿到這個欄位和表中欄位的比較型別,是等於呢或者是大於小於。下一步我們在判斷是否有忽略欄位,因為在一些情況下,我們不希望某些欄位進入篩選條件中,我們只需排除他即可。
最後就easy了,我們只要薅一層mybatis-plus的羊毛即可,利用反射的方式,將相關引數天好,返回一個可以被執行的QueryWrapper。至此,我們輕輕鬆鬆拿到了我們之前寫了半天抹布程式碼的QueryWrapper,剩下的直接用或者進一步操作就看具體的需求咯。
最爽的是這種薅mybatis-plus羊毛方式的做法擴充套件性極強,但凡它支援的比較符號我基本都能支援,以後哪怕他怎麼更新也是萬變不離其宗,任爾東西南北風,我自微風清清。
下面給一個使用例項。
@Data
public class MovementPo extends BasePagePo {
//動作名稱
@Compare(compareType = "like")
private String movementName;
//動作英文名稱
private String movementEnName;
//動作難度
private String movementLevel;
//適用性別 性別,1:男,2:女,0:通用
@Compare(compareType = "eq")
private Integer applicableGender;
//動作型別 0:無氧,1:有氧
private Integer movementType;
//起始時間
private Date startTime;
//結束時間
@Compare(compareType = "le",columnName = "gmt_create")
private Date endTime;
}
@Test
public void test() throws Exception {
//構造條件
MovementPo po = new MovementPo();
po.setMovementName("高抬腿");
po.setApplicableGender(0);
po.setEndTime(new Date());
//忽略比較的欄位,可不傳
ArrayList<String> ignores = new ArrayList<>();
ignores.add("applicable_gender");
//生成wrapper
QueryWrapper<Movement> wrapper = MpUtil.generateWrapper(Movement.class, po, ignores);
List<Movement> list = movementService.list(wrapper);
list.forEach(System.out::println);
}
Movement{,id=62,movementName=高抬腿,movementLevel=3,applicableGender=1,movementType=1,exercisePart=[10,9,8],exerciseMuscle=[[1,12],[2,15],[2,16],[2,17],[3,18],[3,19],[3,20],[4,21],[4,22],[5,23],[5,24],[5,25],[5,42],[6,26],[6,27],[6,28],[6,29],[6,30]],requireMachine=[30,29,28],heatConsumption=12,video=12,videoDuration=12,cover=34,movementIntroduction=<h1>21</h1>,remark=null,isDelete=0,gmtModified=Thu Jun 29 09:44:34 CST 2023,gmtCreate=Sat Jun 17 16:52:55 CST 2023}
Movement{,id=63,movementName=高抬腿,movementLevel=3,applicableGender=1,movementType=1,exercisePart=[10,9,8],exerciseMuscle=[[1,12],[2,15],[2,16],[2,17],[3,18],[3,19],[3,20],[4,21],[4,22],[5,23],[5,24],[5,25],[5,42],[6,26],[6,27],[6,28],[6,29],[6,30]],requireMachine=[30,29,28],heatConsumption=12,video=12,videoDuration=12,cover=34movementIntroduction=<h1>21</h1>,remark=null,isDelete=0,gmtModified=Thu Jun 29 09:44:34 CST 2023,gmtCreate=Sat Jun 17 16:52:55 CST 2023}
有興趣的老少年們可以自己去體驗一下,程式碼不多,用的很爽。