一、编写JPA复杂分页查询由来

​ 新公司项目中使用的ORM框架为JPA框架,但是我们后端写的分页查询接口都各不相同。存在扩展性差、支持的查询类型单一、无法复用等问题。

​ 所以我在写分页查询的进行了一些设计,将分页查询设计成了可拓展、功能复杂的一个公共分页查询方法。该公共方法所有使用JPA框架的项目都可以使用。

二、设计思路

2.1 、请求参数设计

​ 首先复用性高,首先想到使用反射或者泛型来实现。

​ 复杂的查询类型,可以想到的精确查询、模糊查询、批量查询、段查询这些。

​ 除了查询功能支持,还需要有分页相关的参数,然后还要能够支持排序功能。

​ 所以再设计分页接口请求参数时需要考虑能够满足上面能够功能,最终设计出来的分页请求参数PageParam如下。

2.2、处理请求参数

​ 由于使用的是JPA框架,用过这个框架的同学都知道这个框架的查询都是通过实现JpaRepository<T, ID>接口来完成的。下面列举一下常用的查询手段,

​ 1、通过Example.of()构造查询对象,这个只能进行精确查询。

​ 2、通过方法命名形式进行查询,eg findAllByxxxxAndxxxxInAndxxxxIsTrue()。这个支持的查询很多但是对命名规范有要求且如果查询条件过多,方法名就很长很长了。

​ 3、使用@Query完成较为复杂的查询,方法名不会很长。但是扩展性、复用性差,该查询条件就得改动查询方法。

​ 4、Specification,这个就是本文实现的关键,通过Specification构造复杂查询条件进行查询。如果不了解Specification的用法建议先去了解一下其用法在继续浏览下文。

​ 具体构造实现请跳转构造查询条件

三、实际使用

​ 处理完成之后实际处理起来就比较简单了。如果还有什么疑问可以邮件私我,邮箱号在最下面。

/**
 * 分页查询 
 * @param pageParam  查询条件
 * @return
 */
@Override
public Page<XXXXVO> page(PageParam<XXXXVO> pageParam) {
    XXXXVO vo = pageParam.getVo();
    pageParam.getSorts().put("updateDate", JpaUtils.SORT_DESC);
    if (null == vo) {
        vo = new XXXXVO();
    }
    Pageable pageable = jpaUtils.getPageable(pageParam);
    //vo转po
    XXXXPO entity = DozerUtil.transfor(vo, XXXXPO.class);
    //这个就是前面实现的构造查询条件方法
    Specification<XXXXPO> spec = jpaUtils.getSpec(entity, pageParam);
    //dao接口用过jpa的都清楚,实现了JpaRepository用来的接口
    //如果你的dao没有这个方法,dao可以实现一个自己声明的接口(eg:BaseJpaRepository)实现JpaRepository,在里面加上入参为这两个的方法即可。
    Page<XXXXPO> page = XXXXDao.findAll(spec, pageable);
    List<XXXXPO> all = page.getContent();
    return new PageImpl<>(DozerUtil.transforList(all, XXXXVO.class), page.getPageable(), page.getTotalElements());
}

四、附录代码部分

4.1、PageParam
/**
 * 分页查询请求参数
 * @author hehuibing442@163.com
 * @version 2.0.0
 * @date 2022/05/17 09:48
 * @description
 */
@Data
public class PageParam<T> implements Serializable {
    @ApiModelProperty("分页查询对象,主要用于精确查询")
    private T vo;
    @ApiModelProperty("页码,如果不传默认1")
    @JsonProperty("page_index")
    private Integer pageIndex =1;
    @ApiModelProperty("页数,如果不传默认10")
    @JsonProperty("page_size")
    private Integer pageSize =10;
    @ApiModelProperty("排序方式,只支持asc和desc. eg: \"create_date\":\"desc/asc\" ")
    private Map<String,String> sorts =new HashMap<>();
    @JsonProperty("search_date_map")
    @ApiModelProperty("Date类型日期段查询,eg createDate:[startDate,endDate]")
    private Map<String,List<String>> searchDateMap =new HashMap<>();
    @JsonProperty("search_local_time_date_map")
    @ApiModelProperty("LocalTimeDate类型日期段查询, eg createDate:[startDate,endDate]")
    private Map<String,List<String>> searchLocalTimeDateMap =new HashMap<>();
    @JsonProperty("search_map")
    @ApiModelProperty("查询map, eg id:{in:1,2,3}")
    private Map<String, SearchFilter> searchMap =new HashMap<>();
}
/**
 * 搜索过滤对象
 * @author hehuibing442@163.com
 * @version 2.0.0
 * @date 2022/05/17 11:23
 * @description
 */
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SearchFilter {
    @ApiModelProperty("查询方式,目前支持:like,in")
    private String opt;
    @ApiModelProperty("查询值,多个值用英文逗号‘,’分割 ")
    private String values;
}
4.2、构造查询条件
4.2.1、精确条件构造
//不需要权限控制的方法
public <T, R> Specification<T> getSpec(T entity, PageParam<R> pageParam) {
    return this.getSpec(entity, pageParam, false);
}
/**
* 构造查询条件
* @param entity 实体Model类,拥有@Entity与@Table注解的实体, 非DTO、VO等
* @param pageParam 分页查询请求参数
* @param isOpenAuth 是否开启权限控制,属于扩展功能
* @param <T>
* @param <R>
* @return
*/
public <T,R> Specification<T> getSpec(T entity, PageParam<R> pageParam,boolean isOpenAuth){
        if (null==pageParam||null==entity){
            throw new BusinessException("查询体不能为空");
        }
        return (root,cq,cb)->{
            //查询谓词集合
            List<Predicate> predicates = new ArrayList<>();
            //对象构造--就是获取实体类及其父类的所有声明字段
            List<Field> fields = this.getFields(entity);
            //字段map--方便检索Field,提升响应速度
            Map<String, Field> fieldMap = fields.stream().collect(Collectors.toMap(Field::getName, v -> v));
            //构造精确查询条件
            for (Field field:fields){
                String name = field.getName();
                Class<?> type = field.getType();
                if (!field.isAccessible()){
                    field.setAccessible(true);
                }
                Object fieldValue = this.getFieldValue(entity, field);
                if (null==fieldValue|| ObjectUtils.isEmpty(fieldValue)){
                    continue;
                }
                //校验
                if (!this.isSupportType(type)){
                    throw new BusinessException(type+"类型暂不支持!");
                }
                //将构造的查询条件加入谓词集合
                predicates.add(cb.equal(root.get(this.underlineToHump(name)).as(field.getType()), fieldValue));
            }
          	//下面这些均可抽取为一个方法。为了防止篇幅过长将分别列出
            //是否开启权限控制 
            //排序
            //模糊查询
            //时间段查询
        }
4.2.2权限控制构造

​ 如果使用的基于角色RBAC或者基于属性ABAC这种方式实现的权限控制,在需要用到数据权限的时候就需要通过当前用户信息,判断当前用户所在角色组权限或者相关属性拿到当前用户针对当前查询数据的过滤条件。将得到的过滤条件比如XXXX部门、XXXX小组转、XXXX岗位换成对应的查询条件。中间可能涉及到多次转换,但最终一定可以转换成实体类里面的用于权限控制的公共字段。

​ 我这里使用的是比较简单的权限控制,只根据用户名进行权限控制。可以参考,

//是否开启权限控制--只有属于继承了公共字段才可以生效
if (isOpenAuth && entity instanceof AbstractEntityPO) {
    //可供扩展
    String nickName = HttpRequestUtil.getNickNameOrThrow();
    Field field = fieldMap.get(PageUtils.CREATE_USER);
    if (null != field) {
        predicates.add(cb.equal(root.get(this.underlineToHump(field.getName())).as(field.getType()), nickName));
    }
}
4.2.3、模糊查询与批量查询条件构造
//searchMap构造
Map<String, SearchFilter> searchMap = pageParam.getSearchMap();
//复杂查询构造
if (searchMap.size()>0){
    searchMap.forEach((fieldName, searchFilter) -> {
        if (null != searchFilter&& StringUtils.isNotEmpty(searchFilter.getValues())) {
        	//校验字段是否存在
            this.checkFieldsExist(fieldMap,fieldName);
            String opt = searchFilter.getOpt();
            String optValues = searchFilter.getValues();
            //in查询, IN,LIKE均为自定义的常量
            if (IN.equals(opt)) {
                String[] values = optValues.split(",");
                CriteriaBuilder.In<String> in = cb.in(root.get(this.underlineToHump(fieldName)).as(String.class));
                for (String value : values) {
                    in.value(value);
                }
                predicates.add(cb.and(in));
            }
            //like查询
            if (LIKE.equals(opt)){
                predicates.add(cb.like(root.get(this.underlineToHump(fieldName)).as(String.class),"%"+optValues+"%"));
            }
        }
    });
};
4.2.4、时间段查询条件构造
//日期查询构造
//LocalDateTime构造
Map<String, List<String>> localDateTimeMap = pageParam.getSearchLocalTimeDateMap();
if (localDateTimeMap.size()>0){
    localDateTimeMap.forEach((field,dates)->{
        if (!CollectionUtils.isEmpty(dates)){
            this.checkFieldsExist(fieldMap,field);
            if(DATE_SEARCH_LIST_LENGTH!=dates.size()){
                throw new BusinessException("构造日期查询条件失败!");
            }
            //这里就是将字符串格式的日期转为指定类型的日期
            LocalDateTime startDate = PageUtils.parseLocalDateTime(dates.get(0));
            LocalDateTime endDate = PageUtils.parseLocalDateTime(dates.get(1));
            if (startDate.isAfter(endDate)){
                throw new BusinessException("构造日期查询条件失败");
            }
            //构造日期段查询,除了用between还可以考虑联合使用ge与le实现
            predicates.add(cb.between(root.get(this.underlineToHump(field)).as(LocalDateTime.class),startDate,endDate));
        }
    });
}
4.2.5、排序条件构造
//排序参数构造
Map<String, String> sorts = pageParam.getSorts();
if (sorts.size() > 0) {
    //排序集合
    List<Order> sortOrders = sorts.entrySet().stream()
        .map(entry -> {
            String field = entry.getKey();
            this.checkFieldsExist(fieldMap, field);
            String sortType = entry.getValue();
            if (SORT_DESC.equals(sortType)) {
                return cb.desc(root.get(this.underlineToHump(field)));
            }
            return cb.asc(root.get(this.underlineToHump(field)));
    	})
        .collect(Collectors.toList());
    cq.orderBy(sortOrders);
}
4.2.6、 公共方法
/**
 * 校验字段是否是存在的
 *
 * @param fieldMap
 * @param fieldName
 */
private void checkFieldsExist(Map<String, Field> fieldMap, String fieldName) {
    if (!fieldMap.containsKey(this.underlineToHump(fieldName))) {
        throw new BusinessException("查询条件[" + fieldName + "]名称不合法");
    }
}
/**
 * 判读当前类是否是支持的类型
 *
 * @param type
 * @return
 */
private boolean isSupportType(Class<?> type) {
    if (String.class.equals(type)) {
        return true;
    }
    if (LocalDateTime.class.equals(type)) {
        return true;
    }
    if (BigDecimal.class.equals(type) || Double.class.equals(type) || Float.class.equals(type)) {
        return true;
    }
    if (Boolean.class.equals(type)) {
        return true;
    }
    if (Integer.class.equals(type) || int.class.equals(type)) {
        return true;
    }
    if (Date.class.equals(type)) {
        return true;
    }
    return false;
}
/**
 * 根据传入的带下划线的字符串转化为驼峰格式
 *
 * @param str
 * @return
 * @author mrf
 */
private String underlineToHump(String str) {
    //正则匹配下划线及后一个字符,删除下划线并将匹配的字符转成大写
    Matcher matcher = UNDERLINE_PATTERN.matcher(str);
    StringBuffer sb = new StringBuffer(str);
    if (matcher.find()) {
        sb = new StringBuffer();
        //将当前匹配的子串替换成指定字符串,并且将替换后的子串及之前到上次匹配的子串之后的字符串添加到StringBuffer对象中
        //正则之前的字符和被替换的字符
        matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
        //把之后的字符串也添加到StringBuffer对象中
        matcher.appendTail(sb);
    } else {
        //去除除字母之外的前面带的下划线
        return sb.toString().replaceAll("_", "");
    }
    return underlineToHump(sb.toString());
}

发表回复