基于JpaSpecification实现的复杂分页查询
新公司项目中使用的ORM框架为JPA框架,但是我们后端写的分页查询接口都各不相同。存在扩展性差、支持的查询类型单一、无法复用等问题。
所以我在写分页查询的进行了一些设计,将分页查询设计成了可拓展、功能复杂的一个公共分页查询方法。该公共方法所有使用JPA框架的项目都可以使用。
二、设计思路2.1 、请求参数设计 首先复用性高,首先想到使用反射或者泛型来实现。
复杂的查询类型,可以想到的精确查询、模糊查询、批量查询、段查询这些。
除了查询功能支持,还需要有分页相关的参数,然后还要能够支持排序功能。
所以再设计分页接口请求参数时需要考虑能够满足上面能够功能,最终设计出来的分页请求参数PageParam如下。
2.2、处理请求参数 由于使用的是JPA框架,用过这个框架的同学都知道这个框架的查询都是通过实现JpaRepository接口来完成的。下面列举一下常用的查询手段,
1、通过Example.of()构造查询对象,这个只能进行精确查询。
2、通过方法命名形式进行查询,eg findAllByxxxxAndxxxxInAndxxxxIsTrue()。这个支持的查询很多但是对命名规范有要求且如果查询条件过多,方法名就很长很长了。
3、使用@Query完成较为复杂的查询,方法名不会很长。但是扩展性、复用性差,该查询条件就得改动查询方法。
4、Specification,这个就是本文实现的关键,通过Specification构造复杂查询条件进行查询。如果不了解Specification的用法建议先去了解一下其用法在继续浏览下文。
具体构造实现请跳转构造查询条件
三、实际使用 处理完成之后实际处理起来就比较简单了。如果还有什么疑问可以邮件私我,邮箱号在最下面。
/*** 分页查询* @param pageParam 查询条件* @return*/@OverridepublicPage<XXXXVO>page(PageParam<XXXXVO>pageParam){XXXXVO vo=pageParam.getVo();pageParam.getSorts().put("updateDate",JpaUtils.SORT_DESC);if(null==vo){vo=newXXXXVO();}Pageablepageable=jpaUtils.getPageable(pageParam);//vo转poXXXXPO 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();returnnewPageImpl<>(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*/@DatapublicclassPageParam<T>implementsSerializable{@ApiModelProperty("分页查询对象,主要用于精确查询")privateT vo;@ApiModelProperty("页码,如果不传默认1")@JsonProperty("page_index")privateIntegerpageIndex=1;@ApiModelProperty("页数,如果不传默认10")@JsonProperty("page_size")privateIntegerpageSize=10;@ApiModelProperty("排序方式,只支持asc和desc. eg: \"create_date\":\"desc/asc\" ")privateMap<String,String>sorts=newHashMap<>();@JsonProperty("search_date_map")@ApiModelProperty("Date类型日期段查询,eg createDate:[startDate,endDate]")privateMap<String,List<String>>searchDateMap=newHashMap<>();@JsonProperty("search_local_time_date_map")@ApiModelProperty("LocalTimeDate类型日期段查询, eg createDate:[startDate,endDate]")privateMap<String,List<String>>searchLocalTimeDateMap=newHashMap<>();@JsonProperty("search_map")@ApiModelProperty("查询map, eg id:{in:1,2,3}")privateMap<String,SearchFilter>searchMap=newHashMap<>();}/*** 搜索过滤对象* @author hehuibing442@163.com* @version 2.0.0* @date 2022/05/17 11:23* @description*/@Data@Accessors(chain=true)@NoArgsConstructor@AllArgsConstructorpublicclassSearchFilter{@ApiModelProperty("查询方式,目前支持:like,in")privateStringopt;@ApiModelProperty("查询值,多个值用英文逗号‘,’分割 ")privateStringvalues;}4.2、构造查询条件4.2.1、精确条件构造//不需要权限控制的方法public<T,R>Specification<T>getSpec(T entity,PageParam<R>pageParam){returnthis.getSpec(entity,pageParam,false);}/*** 构造查询条件* @param entity 实体Model类,拥有@Entity与@Table注解的实体, 非DTO、VO等* @param pageParam 分页查询请求参数* @param isOpenAuth 是否开启权限控制,属于扩展功能* @param * @param * @return*/public<T,R>Specification<T>getSpec(T entity,PageParam<R>pageParam,booleanisOpenAuth){if(null==pageParam||null==entity){thrownewBusinessException("查询体不能为空");}return(root,cq,cb)->{//查询谓词集合List<Predicate>predicates=newArrayList<>();//对象构造--就是获取实体类及其父类的所有声明字段List<Field>fields=this.getFields(entity);//字段map--方便检索Field,提升响应速度Map<String,Field>fieldMap=fields.stream().collect(Collectors.toMap(Field::getName,v->v));//构造精确查询条件for(Fieldfield:fields){Stringname=field.getName();Classtype=field.getType();if(!field.isAccessible()){field.setAccessible(true);}ObjectfieldValue=this.getFieldValue(entity,field);if(null==fieldValue||ObjectUtils.isEmpty(fieldValue)){continue;}//校验if(!this.isSupportType(type)){thrownewBusinessException(type+"类型暂不支持!");}//将构造的查询条件加入谓词集合predicates.add(cb.equal(root.get(this.underlineToHump(name)).as(field.getType()),fieldValue));}//下面这些均可抽取为一个方法。为了防止篇幅过长将分别列出//是否开启权限控制//排序//模糊查询//时间段查询}4.2.2权限控制构造 如果使用的基于角色RBAC或者基于属性ABAC这种方式实现的权限控制,在需要用到数据权限的时候就需要通过当前用户信息,判断当前用户所在角色组权限或者相关属性拿到当前用户针对当前查询数据的过滤条件。将得到的过滤条件比如XXXX部门、XXXX小组转、XXXX岗位换成对应的查询条件。中间可能涉及到多次转换,但最终一定可以转换成实体类里面的用于权限控制的公共字段。
我这里使用的是比较简单的权限控制,只根据用户名进行权限控制。可以参考,
//是否开启权限控制--只有属于继承了公共字段才可以生效if(isOpenAuth&&entityinstanceofAbstractEntityPO){//可供扩展StringnickName=HttpRequestUtil.getNickNameOrThrow();Fieldfield=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);Stringopt=searchFilter.getOpt();StringoptValues=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(Stringvalue: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()){thrownewBusinessException("构造日期查询条件失败!");}//这里就是将字符串格式的日期转为指定类型的日期LocalDateTimestartDate=PageUtils.parseLocalDateTime(dates.get(0));LocalDateTimeendDate=PageUtils.parseLocalDateTime(dates.get(1));if(startDate.isAfter(endDate)){thrownewBusinessException("构造日期查询条件失败");}//构造日期段查询,除了用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->{Stringfield=entry.getKey();this.checkFieldsExist(fieldMap,field);StringsortType=entry.getValue();if(SORT_DESC.equals(sortType)){returncb.desc(root.get(this.underlineToHump(field)));}returncb.asc(root.get(this.underlineToHump(field)));}).collect(Collectors.toList());cq.orderBy(sortOrders);}4.2.6、 公共方法/*** 校验字段是否是存在的** @param fieldMap* @param fieldName*/privatevoidcheckFieldsExist(Map<String,Field>fieldMap,StringfieldName){if(!fieldMap.containsKey(this.underlineToHump(fieldName))){thrownewBusinessException("查询条件["+fieldName+"]名称不合法");}}/*** 判读当前类是否是支持的类型** @param type* @return*/privatebooleanisSupportType(Classtype){if(String.class.equals(type)){returntrue;}if(LocalDateTime.class.equals(type)){returntrue;}if(BigDecimal.class.equals(type)||Double.class.equals(type)||Float.class.equals(type)){returntrue;}if(Boolean.class.equals(type)){returntrue;}if(Integer.class.equals(type)||int.class.equals(type)){returntrue;}if(Date.class.equals(type)){returntrue;}returnfalse;}/*** 根据传入的带下划线的字符串转化为驼峰格式** @param str* @return* @author mrf*/privateStringunderlineToHump(Stringstr){//正则匹配下划线及后一个字符,删除下划线并将匹配的字符转成大写Matchermatcher=UNDERLINE_PATTERN.matcher(str);StringBuffersb=newStringBuffer(str);if(matcher.find()){sb=newStringBuffer();//将当前匹配的子串替换成指定字符串,并且将替换后的子串及之前到上次匹配的子串之后的字符串添加到StringBuffer对象中//正则之前的字符和被替换的字符matcher.appendReplacement(sb,matcher.group(1).toUpperCase());//把之后的字符串也添加到StringBuffer对象中matcher.appendTail(sb);}else{//去除除字母之外的前面带的下划线returnsb.toString().replaceAll("_","");}returnunderlineToHump(sb.toString());}