JPA 痛点
我个人觉得 Spring Data JPA 基于注解的查询方式确实很好用,但是对于那种动态条件的查询就不怎么友好了,MyBatis 还可以使用 if 标签。(不知道是不是我不会用)
比如:后台的这个查询,查询条件可以输入也可以不输入,这样使用JPA 基于注解和接口方法的方式感觉就不怎么好用了。
那么一般会有如下的查询(当然查询方式很多,我只列举一种)
如果搜索条件更多的话,这个查询还会更复杂,所以这一大坨的东西就值得优化一下了。
优化思路
其实可以看到上面的检索条件很类似,都是通过下面的方式增加条件过滤的。
Predicate equalPredate = criteriaBuilder.equal(root.get("userId").as(Long.class), request.getUserId());
predicates.add(equalPredate);
变化的只是 查询的字段名称,字段类型,以及查询方式(是模糊查询还是等值查询又或者是比较查询等等),那么我觉得改造的关键就是 把变化的抽出来,不变的地方做成模板。
那么变化的是字段名称、类型以及查询方式,字段名称和类型可以从查询入参对象入手,可以通过反射获取到入参对象的字段名称和类型,这样就解决了过滤的条件了,那么查询方式可以通过自定义一个注解携带查询方式信息。
实际实现
自定义注解
package groot.user.behavior.support;
import java.lang.annotation.*;
/**
* @Classname JpaParam
* @Description
* @Date 2021/8/12 15:54
* @Created by wangchangjiu
*/
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JpaParam {
QueryType queryType() default QueryType.EQUAL;
enum QueryType {
IN("in"),
LT("lt"),
LE("le"),
GE("ge"),
GT("gt"),
NOT_LIKE("notLike"),
NOTEQUAL("notEqual"),
EQUAL("equal"),
LIKE("like");
private String queryType;
QueryType(String queryType){
this.queryType = queryType;
}
public String getQueryType() {
return queryType;
}
public void setQueryType(String queryType) {
this.queryType = queryType;
}
}
}
这个 JpaParam 注解有个属性 QueryType ,这个枚举定义了很多查询的类型,比如 “equal” 、“like” 等等。用户指定查询的方式
Repository 工具类
package groot.user.behavior.support;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.util.ReflectionUtils;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Objects;
import java.util.function.Consumer;
/**
* @Classname RepositoryHelper
* @Description
* @Date 2021/8/12 16:27
* @Created by wangchangjiu
*/
public class RepositoryHelper {
/**
* 带参数查询所有
*
* @param executor
* @param paramObj
* @param pageable
* @param <T>
* @return
*/
public static <T> Page<T> findAll(JpaSpecificationExecutor<T> executor, Object paramObj, Pageable pageable) {
Page<T> page = executor.findAll((root, criteriaQuery, criteriaBuilder) -> {
Predicate predicate = criteriaBuilder.and();
ReflectionUtils.doWithFields(paramObj.getClass(), field -> {
JpaParam jpaParam = AnnotationUtils.getAnnotation(field, JpaParam.class);
field.setAccessible(true);
Object paramValue = field.get(paramObj);
Class paramClazz = field.getType();
if (jpaParam != null && Objects.nonNull(paramValue)) {
if (paramValue instanceof String) {
if (StringUtils.isBlank((String) paramValue)) {
return;
}
}
if (jpaParam.queryType() == JpaParam.QueryType.EQUAL) {
// 等于
predicate.getExpressions().add(criteriaBuilder.equal(root.get(field.getName()).as(paramClazz), paramValue));
} else if (jpaParam.queryType() == JpaParam.QueryType.LIKE) {
// 模糊查询
predicate.getExpressions().add(criteriaBuilder.like(root.get(field.getName()).as(paramClazz), "%" + paramValue + "%"));
} else if (jpaParam.queryType() == JpaParam.QueryType.NOT_LIKE) {
predicate.getExpressions().add(criteriaBuilder.notLike(root.get(field.getName()).as(paramClazz), "%" + paramValue + "%"));
} else if (jpaParam.queryType() == JpaParam.QueryType.NOTEQUAL) {
// 不等于
predicate.getExpressions().add(criteriaBuilder.notEqual(root.get(field.getName()).as(paramClazz), paramValue));
} else if (jpaParam.queryType() == JpaParam.QueryType.GT) {
if (!(paramValue instanceof Number)) {
throw new IllegalArgumentException("param value not Number");
}
predicate.getExpressions().add(criteriaBuilder.gt(root.get(field.getName()).as(paramClazz), (Number) paramValue));
} else if (jpaParam.queryType() == JpaParam.QueryType.GE) {
if (paramValue instanceof Date) {
predicate.getExpressions().add(criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), (Date) paramValue));
} else if (!(paramValue instanceof Number)) {
throw new IllegalArgumentException("param value not Number");
} else {
predicate.getExpressions().add(criteriaBuilder.ge(root.get(field.getName()).as(paramClazz), (Number) paramValue));
}
} else if (jpaParam.queryType() == JpaParam.QueryType.LT) {
if (!(paramValue instanceof Number)) {
throw new IllegalArgumentException("param value not Number");
}
predicate.getExpressions().add(criteriaBuilder.lt(root.get(field.getName()).as(paramClazz), (Number) paramValue));
} else if (jpaParam.queryType() == JpaParam.QueryType.LE) {
if (paramValue instanceof Date) {
predicate.getExpressions().add(criteriaBuilder.lessThanOrEqualTo(root.get(field.getName()).as(paramClazz), (Date) paramValue));
} else if (!(paramValue instanceof Number)) {
throw new IllegalArgumentException("param value not Number");
} else {
predicate.getExpressions().add(criteriaBuilder.le(root.get(field.getName()).as(paramClazz), (Number) paramValue));
}
} else if (jpaParam.queryType() == JpaParam.QueryType.IN) {
Path<Object> path = root.get(field.getName());
CriteriaBuilder.In<Object> in = criteriaBuilder.in(path);
if (paramClazz == String.class && String.valueOf(paramValue).contains(",")) {
Arrays.asList(String.valueOf(paramValue).split(",")).stream().forEach(item -> in.value(item));
} else if (Collection.class.isAssignableFrom(paramClazz) && paramValue instanceof Collection) {
for (Object item : Collection.class.cast(paramValue)) {
in.value(item);
}
}
predicate.getExpressions().add(criteriaBuilder.and(in));
}
}
});
return predicate;
}, pageable);
return page;
}
}
这样整个工具类就完成了,具体的代码可以看上面,就不做过多解释了,主要就是 利用反射获取参数字段的类型、参数名称、参数值,还有就是参数字段上的自定义注解 JpaParam,通过这个自定义注解来选择哪种查询方式。
我这里只封装了分页按条件查询数据的情况,其他的没有封装,因为按照这个思路其他的封装起来也是很容易的。
如何使用工具类
上面工具类和优化思路都已经介绍了,接下来看看怎么使用这个工具类来简化开发吧。
案例:我们对上面JPA痛点给出的案例进行改造。
定义查询对象,给查询对象字段增加查询方式
默认查询方式是等值查询。如果字段不需要查询,那就不加
JpaParam 这个注解。
查询业务代码
真正写查询的代码只有这两句,注意,这里传入的
repository 是 一个 JpaRepository,JpaSpecificationExecutor的接口,例如:
怎么样?是不是感觉整个业务代码都干净了,不再是每个查询方法里面都是一大坨查询条件了。这里只是简单的封装一下,因为我们这种情况够用了。