SpringBoot中基于JPA的多条件动态查询封装
最近地摊经济盛行了,对于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带走哈!”
上一篇: TotalUninstall6破解步骤卸载软件更彻底更专业
下一篇: java异常有效实践