最近地攤經濟盛行了,對於wshanshi這個喫貨來說是再幸福不過的了。逛喫,逛喫,逛喫…怕是開心的要飛起來了呢!
要不咱也擺攤賣點啥去?看了看鏡子裏帥氣的面孔,笑容愈發…
“老闆,來一份wshanshi。酸甜,微辣,微辣…”
“好嘞!您稍等!我這就去把wshanshi叫來!”
“小二,來說一下SpringBoot中基於JPA的多條件動態查詢封裝。講不好不但不給飯錢,還要把你們wshanshi小姐姐賠給我!”
“好嘞客官,給您免單!趕緊把她帶走,帶走!把我喫窮了都!”
用過SpringDataJPA的都知道,自定義接口繼承JpaRepository<T,ID>接口、JpaSpecificationExecutor 接口之後,可以按照一定的規則定義接口方法名,實現變着花樣的增刪改查。
源碼如下:
原理在於:自定義接口繼承了JpaRepository<T, ID>和JpaSpecificationExecutor 接口。由於JpaRepository<T, ID> 繼承了 PagingAndSortingRepository<T, ID>接口和QueryByExampleExecutor接口,且PagingAndSortingRepository<T, ID>又繼承了CrudRepository<T, ID>接口。因此可以直接使用默認封裝的增刪改查方法、分頁排序方法、多條件動態查詢方法。
當然你也可以自定義,像常見的根據用戶名查找用戶、根據用戶編號刪除用戶等等。
有興趣的可以去看一下源碼,這裏就不多說了。開始今天的主題:SpringBoot中基於JPA的多條件動態查詢封裝。
JPA中基於Specification的多條件動態查詢,Specification的使用點這裏:SpringDataJPA中使用Specification進行表連接多條件分頁動態查詢。
像下圖這樣,使用Specification時,如果不進行二次封裝,那麼一個項目中可能會寫多個動態條件查詢方法。整體看起來代碼是冗餘的。
這個圖片中的代碼,雖然用法對,寫法也對,但是如果出現過多,不用想絕對會被Diss。別問我怎麼知道的,不信你嘗試。到時候一定要控制住你的右手,離自己的小臉遠一點,力度小一點。哇哈哈哈哈哈…
那麼,爲了避免衝動起來自己扇自己,有沒有更好的辦法,可以看起來既清爽簡潔,使用起來又方便快捷的?
當然,我們可以選擇封裝一下。自定義註解實現多條件動態查詢(可表關聯查詢),使用方便簡潔,清晰明瞭。何樂而不爲呢?
具體操作如下:
一、自定義註解查詢類
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
/**
* 基本對象的屬性名
*/
String propName() default "";
/**
* 基本對象的屬性名LIST OR
*/
String[] orPropNames() default {};
String orPropVal() default "";
/**
* 查詢方式 默認精確查詢
*/
Type type() default Type.EQUAL;
/**
* 多表連接查詢的屬性名,如User類中的role
*
* @return
*/
String joinName() default "";
/**
* 左連接
*
* @return
*/
Join join() default Join.LEFT;
/**
* 多字段模糊搜索,僅支持String類型字段,多個用逗號隔開, 如用戶名和手機號模糊查詢@Query(blurry = "username,phone")
*
* @return
*/
String blurry() default "";
enum Type {
/**
* 相等,精確查詢
*/
EQUAL,
/**
* 大於等於
*/
GREATER_THAN,
/**
* 小於等於
*/
LESS_THAN,
/**
* 左右均模糊查詢:%a%
*/
INNER_LIKE,
/**
* 左模糊查詢:%a
*/
LEFT_LIKE,
/**
* 右模糊查詢: a%
*/
RIGHT_LIKE,
/**
* 小於
*/
LESS_THAN_NQ,
/**
* 在...範圍內
*/
IN,
/**
* 或者
*/
OR
}
enum Join {
/**
* 左連接
*/
LEFT,
/**
* 右連接
*/
RIGHT,
}
}
二、定義Predicate轉換類(該類可Copy直接使用)
該類內部通過反射獲取多條件查詢屬性,進行動態拼接。實現了多條件 精確查詢、模糊查詢、範圍查詢,關聯查詢。基本操作是夠用了。
@Slf4j
public class QueryUtil {
@SuppressWarnings("unchecked")
public static <R, Q> Predicate getPredicate(Root<R> root, Q query, CriteriaBuilder cb) {
List<Predicate> list = new ArrayList<>();
if (query == null) {
return cb.and(list.toArray(new Predicate[list.size()]));
}
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
for (Field field : fields) {
boolean accessible = field.isAccessible();
field.setAccessible(true);
Query q = field.getAnnotation(Query.class);
if (q != null) {
String propName = q.propName();
String joinName = q.joinName();
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Class<?> fieldType = field.getType();
Object val = field.get(query);
if (ObjectUtil.isNull(val) || "".equals(val)) {
continue;
}
Join join = null;
// 模糊多字段
if (ObjectUtil.isNotEmpty(blurry)) {
String[] blurrys = blurry.split(",");
List<Predicate> orPredicate = new ArrayList<>();
for (String s : blurrys) {
orPredicate.add(cb.like(root.get(s)
.as(String.class), "%" + val.toString() + "%"));
}
Predicate[] p = new Predicate[orPredicate.size()];
list.add(cb.or(orPredicate.toArray(p)));
continue;
}
if (ObjectUtil.isNotEmpty(joinName)) {
String[] joinNames = joinName.split(">");
for (String name : joinNames) {
switch (q.join()) {
case LEFT:
if (ObjectUtil.isNotEmpty(join)) {
join = join.join(name, JoinType.LEFT);
} else {
join = root.join(name, JoinType.LEFT);
}
break;
case RIGHT:
if (ObjectUtil.isNotEmpty(join)) {
join = join.join(name, JoinType.RIGHT);
} else {
join = root.join(name, JoinType.RIGHT);
}
break;
}
}
}
switch (q.type()) {
case EQUAL:
list.add(cb.equal(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), val));
break;
case GREATER_THAN:
list.add(cb.greaterThanOrEqualTo(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN:
list.add(cb.lessThanOrEqualTo(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN_NQ:
list.add(cb.lessThan(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case INNER_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), "%" + val.toString() + "%"));
break;
case LEFT_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), "%" + val.toString()));
break;
case RIGHT_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), val.toString() + "%"));
case IN:
if (CollUtil.isNotEmpty((Collection<Long>) val)) {
list.add(getExpression(attributeName, join, root).in((Collection<Long>) val));
}
break;
case OR:
if (Boolean.valueOf(q.orPropVal())) {
Predicate[] pArrays = (Predicate[]) Arrays.asList(q.orPropNames()).stream().map(
prop -> cb
.equal(getExpression(prop, null, root).as(String.class),
q.orPropVal())).toArray();
cb.or(pArrays);
}
break;
}
}
field.setAccessible(accessible);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return cb.and(list.toArray(new Predicate[list.size()]));
}
@SuppressWarnings("unchecked")
private static <T, R> Expression<T> getExpression(String attributeName, Join join, Root<R> root) {
if (ObjectUtil.isNotEmpty(join)) {
return join.get(attributeName);
} else {
return root.get(attributeName);
}
}
@SuppressWarnings("unchecked")
public static boolean isBlank(final CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (Character.isWhitespace(cs.charAt(i)) == false) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
private static List<Field> getAllFields(Class clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
三、自定義條件查詢類UserQueryCriteria
需要什麼查詢條件在這裏面加。
注意:propName是你實體類中對應的屬性名,需和實體類屬性一致。 如果propName省略不寫,默認按照該查詢類中你定義的屬性名去查詢。如果你既沒定義propName,同時屬性名又寫錯,就會出錯了。
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserQueryCriteria {
// 精確查詢
@Query(propName = "id",type = Query.Type.EQUAL)//propName的值和實體類中屬性值一致
private Long id;
// 左右模糊查詢:%username%
@Query(propName = "username",type = Query.Type.INNER_LIKE)//propName的值和實體類中屬性值一致
private String username;//實體類中用戶名就叫做username
// 精確查詢
@Query(type = Query.Type.EQUAL)
private String email;
// 關聯查詢 joinName是外鍵名 propName是屬性名(必須與實體類中屬性名對應)type是查詢方式,此處是關聯單屬性查詢
// @Query(joinName = "", propName = "",type = Query.Type.EQUAL)
// 範圍查詢,此處是關聯單屬性範圍查詢。如:查詢id 在(1,2,3)中的對象
// @Query(joinName = "", propName = "", type = Query.Type.IN)
// 日期範圍查詢定義 type GREATER_THAN LESS_THAN就好
}
四、 UserService接口
可定義多條件分頁查詢,傳入查詢對象和分頁Pageable對象。
五、 UserServiceImpl中方法實現
@Override
public Map<String, Object> queryAll(UserQueryCriteria criteria, Pageable pageable) {
Page<User> page = userRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryUtil
.getPredicate(root, criteria, criteriaBuilder), pageable);
Map map = PageUtil.toPage(page);
return map;
}
PageUtil
public class PageUtil extends cn.hutool.core.util.PageUtil {
/**
* List 分頁
*
* @param page
* @param size
* @param list
* @return
*/
public static List toPage(int page, int size, List list) {
int fromIndex = page * size;
int toIndex = page * size + size;
if (fromIndex > list.size()) {
return new ArrayList();
} else if (toIndex >= list.size()) {
return list.subList(fromIndex, list.size());
} else {
return list.subList(fromIndex, toIndex);
}
}
/**
* list轉page
*
* @param list
* @param pageable
* @param <T>
* @return
*/
public static <T> Page<T> listConvertToPage(List<T> list, Pageable pageable) {
int start = (int) pageable.getOffset();
int end = (start + pageable.getPageSize()) > list.size() ? list.size() : (start + pageable.getPageSize());
return new PageImpl<T>(list.subList(start, end), pageable, list.size());
}
/**
* Page 數據處理,預防redis反序列化報錯
*
* @param page
* @return
*/
public static Map toPage(Page page) {
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("content", page.getContent());
map.put("totalElements", page.getTotalElements());
return map;
}
/**
* @param object
* @param totalElements
* @return
*/
public static Map toPage(Object object, Object totalElements) {
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("content", object);
map.put("totalElements", totalElements);
return map;
}
}
六、Controller中定義
查詢時根據需求傳入查詢條件和分頁信息。
到此就可以了。只需要在查詢類中屬性上方添加對應的查詢註解,傳入接口就好了。不用再寫好長好長的代碼,減少冗餘。是不是happy多了。
“打烊了,打烊了!”
“客官慢走,別忘了把wshanshi帶走哈!”