SpringDataJPA之Specification复杂查询实战
作者:Moshow郑锴
SpringDataJPA Specification复杂查询
前言
继上次SpringData-JPA之ExampleMatcher实例查询使用一会之后发现ExampleMatcher对日期的查询特别糟糕,所以才有了Specification查询的研究。
- 20200114:更新对JpaSpecificationExecutor的解析,Specification思路2,以及CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery查询部分
- 20180811:根据所学所用,重新更新了文章,并增加了Pageable分页排序功能。
实现
对应的Repository需要实现JpaSpecificationExecutor接口
public interface EventRepository extends JpaRepository<Event, Integer> , JpaSpecificationExecutor<Event>{
Specification与Controller业务逻辑
@GetMapping("/event/list") public ApiReturnObject findAllEvent(String eventTitle,Timestamp registerTime,Integer pageNumber,Integer pageSize) { if(pageNumber==null) pageNumber=1; if(pageSize==null) pageNumber=10; //分页 //Pageable是接口,PageRequest是接口实现,new PageRequest()是旧方法,PageRequest.of()是新方法 //PageRequest.of的对象构造函数有多个,page是页数,初始值是0,size是查询结果的条数,后两个参数参考Sort对象的构造方法 Pageable pageable = PageRequest.of(pageNumber,pageSize,Sort.Direction.DESC,"id"); //Specification查询构造器 Specification<Event> specification=new Specification<Event>() { private static final long serialVersionUID = 1L; @Override public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { Predicate condition1 = null; if(StringUtils.isNotBlank(eventTitle)) { condition1 = criteriaBuilder.like(root.get("eventTitle"),"%"+eventTitle+"%"); }else { condition1 = criteriaBuilder.like(root.get("eventTitle"),"%%"); } Predicate condition2 = null; if(registerTime!=null) { condition2 = criteriaBuilder.greaterThan(root.get("registerTime"), registerTime); }else { condition2 = criteriaBuilder.greaterThan(root.get("registerTime"), new Timestamp(1514736000000L)); } //Predicate conditionX=criteriaBuilder.and(condition1,condition2); //query.where(conditionX); query.where(condition1,condition2); //query.where(getPredicates(condition1,condition2)); //这里可以设置任意条查询条件 return null; //这种方式使用JPA的API设置了查询条件,所以不需要再返回查询条件Predicate给Spring Data Jpa,故最后return null } }; Page<Event> list=eventRepository.findAll(specification, pageable); return ApiReturnUtil.page(list); }
ApiReturnUtil.page封装
其实这个大家根据自己的项目自己封装,这部分不作为核心内容,知识之前有部分网友比较纠结这个工具,所以简单放出来参考一下.
public static ApiReturnObject page(Page returnObject) { return new ApiReturnObject(returnObject.getNumber()+"",returnObject.getNumberOfElements()+"",returnObject.getTotalElements()+"",returnObject.getTotalPages()+"","00","success",returnObject.getContent()); }
ApiReturnObject的主要内容:
String errorCode="00"; Object errorMessage; Object returnObject; String pageNumber; String pageSize; String totalElements; String totalPages; public ApiReturnObject(String pageNumber,String pageSize,String totalElements,String totalPages,String errorCode, Object errorMessage, Object returnObject) { super(); this.pageNumber = pageNumber; this.errorCode = errorCode; this.errorMessage = errorMessage; this.returnObject = returnObject; this.pageSize = pageSize; this.totalElements = totalElements; this.totalPages = totalPages; }
查询效果
返回对象有用的是pageNumber、pageSize、totalElements、totalPages等属性,可对其进行封装
{ "errorCode": "00", "errorMessage": "success", "pageNumber": "1", "pageSize": "2", "returnObject": [ { "eventTitle": "1111", "id": 3, "registerTime": 1528702813000, "status": "0" }, { "eventTitle": "小明失踪", "id": 2, "registerTime": 1526268436000, "status": "0" } ], "totalElements": "5", "totalPages": "3" }
可以查询了。网上关于这个的资料也很少。希望可以帮到大家。
可能遇到的错误
Unable to locate Attribute with the the given name [event] on this ManagedType [org.microservice.tcbj.yytsg.checkcentersys.entity.Event]
出现这样的情况,一般是因为实体类中没有这个属性,例如我Event的是eventTitle,写成了event,就会报错。
JpaSpecificationExecutor接口
20200114补充
JPA 提供动态接口JpaSpecificationExecutor,利用类型检查的方式,利用Specification进行复杂的条件查询,比自己写 SQL 更加便捷和安全.
public interface JpaSpecificationExecutor<T> { /** * Returns a single entity matching the given {@link Specification}. * * @param spec * @return */ T findOne(Specification<T> spec); /** * Returns all entities matching the given {@link Specification}. * * @param spec * @return */ List<T> findAll(Specification<T> spec); /** * Returns a {@link Page} of entities matching the given {@link Specification}. * * @param spec * @param pageable * @return */ Page<T> findAll(Specification<T> spec, Pageable pageable); /** * Returns all entities matching the given {@link Specification} and {@link Sort}. * * @param spec * @param sort * @return */ List<T> findAll(Specification<T> spec, Sort sort); /** * Returns the number of instances that the given {@link Specification} will return. * * @param spec the {@link Specification} to count instances for * @return the number of instances */ long count(Specification<T> spec); }
Specification
Specification是我们传入进去的查询参数,是一个接口,并且只有一个方法
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); }
一个一目了然的方法
第二个实现思路:听说这个方法已经过时了,其实这个方法是最好理解的.这里附上作为思路参考.
public Page<Even> findAll(SearchEven even) { Specification<Even> specification = new Specifications<Even>() .eq(StringUtils.isNotBlank(even.getId()), "id", even.getId()) .gt(Objects.nonNull(even.getStatus()), "status", 0) .between("registerTime", new Range<>(new Date()-1, new Date())) .like("eventTitle", "%"+even.getEventTitle+"%") .build(); return personRepository.findAll(specification, new PageRequest(0, 15)); }
Criteria+TypedQuery
思路三:利用EntityManager相关的CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery进行查询.
@PersistenceContext private EntityManager em; /** * CriteriaBuilder 安全查询创建工厂,创建CriteriaQuery,创建查询具体具体条件Predicate * @author zhengkai.blog.csdn.net */ @Override public List<Even> list(Even even) { //查询工厂 CriteriaBuilder cb = em.getCriteriaBuilder(); //查询类 CriteriaQuery<Even> query = cb.createQuery(Even.class); //查询条件 List<Predicate> predicates = new LinkedList<>(); //查询条件设置 predicates.add(cb.equal("id", even.getId())); predicates.add(cb.like("eventTitle", even.getEventTitle())); //拼接where查询 query.where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); //用JPA 2.0的TypedQuery进行查询 TypedQuery<Even> typedQuery = em.createQuery(query); return typedQuery.getResultList(); }
开发过程中JPA Specification的应用
Specification算是JPA里面比较灵活的查询规范了,方便实现复杂的查询方式。
为什么需要Specification
Spring-Data JPA 本身支持了比较简单的查询方式,也就是根据属性名成结合一些规范编写查询方法,例如,一个Customer对象有name属性,那么如果想要实现根据name来查询,只需要在接口文件中添加一个方法findByName(String name)即可实现。
public interface CustomerRepository extends JpaRepository<Customer, Long> { Customer findByName(String name); Customer findByEmailAddress(String emailAddress); List<Customer> findByLastname(String lastname, Sort sort); Page<Customer> findByFirstname(String firstname, Pageable pageable); }
但是在许多情况下,会有比较复杂的查询,那么这个时候通过自动生成查询方法的方式就不再可行。
应用场景
为了实现复杂查询,JPA提供了Criteria接口,这个是一套标准接口,来看一个例子,在一个平台中,当一个老客户(注册以来两年)生日的时候,系统想要发送一个优惠券给该用户,那么传统使用 JPA 2.0 Criteria API 去实现:
LocalDate today = new LocalDate(); CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<Customer> query = builder.createQuery(Customer.class); Root<Customer> root = query.from(Customer.class); Predicate hasBirthday = builder.equal(root.get(Customer_.birthday), today); Predicate isLongTermCustomer = builder.lessThan(root.get(Customer_.createdAt), today.minusYears(2); query.where(builder.and(hasBirthday, isLongTermCustomer)); em.createQuery(query.select(root)).getResultList();
- 首先获得时间,去比较用户的注册时间
- 接下来是获得JPA中查询使用的实例
- 设置查询条件,首先判断今天是否为某个客户的生日,然后判断是否为老客户
- 执行查询条件,获得满足条件的用户
这里面的主要问题就是代码扩展性比较差,因为需要设置CriteriaBuilder, CriteriaQuery, Root,同时这部分的代码可读性比较差。
JPA Specification实现复杂查询
Specification为了实现可重用的断言,JPA 里面引入了一个Specification接口,接口的封装很简单,如下
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }
在Java8中,我们可以非常方便地实现如上使用Criteria实现的效果
public CustomerSpecifications { public static Specification<Customer> customerHasBirthday() { return (root, query, cb) -> { return cb.equal(root.get(Customer_.birthday), today); }; } public static Specification<Customer> isLongTermCustomer() { return (root, query, cb) -> { return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2)); }; } }
这样对应JPA的repository实现就可以如下
customerRepository.findAll(hasBirthday()); customerRepository.findAll(isLongTermCustomer());
而其实Specification为我们做的事情就是替我们准备了CriteriaQuery, Root, CriteriaBuilder, 有了这些可重用的断言之后,便可以将他们组合起来实现更加复杂的查询了
customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));
JPA多条件、多表查询
如果需要使用Specification,那么对应的Repository需要实现接口JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {}
单表多条件查询
在结合 Spring Boot 和 JPA 之后,为了四号线多条件查询,并且整理分页, 则可以考虑使用Predicate 断言, 例如现在针对 User , 想要根据用户的不同属性进行模糊查询,同时如果属性值为空或者空字符串,则跳过该属性,不作为查询条件,同时属于单表多条件查询,则
//在spring-jpa 2之后 不再使用 new PageRuest(page, pageSize) 的方式 Pageable pageable = PageRequest.of(page, pageSize); //实现条件查询,组合查询 Specification<User> specification = new Specification<User>() { private static final long serialVersionUID = 1L; @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { String account = request.getAccount(); String name = request.getName(); String phone = request.getPhone(); String accountType = request.getAccountType(); String city = request.getCity(); String type = request.getType(); //用列表装载断言对象 List<Predicate> predicates = new ArrayList<Predicate>(); if(org.apache.commons.lang3.StringUtils.isNotBlank(name)) { //模糊查询,like Predicate predicate = cb.like(root.get("name").as(String.class), "%" + name +"%"); predicates.add(predicate); } if (StringUtils.isNotBlank(account)) { Predicate predicate = cb.like(root.get("account").as(String.class), "%" + account +"%"); predicates.add(predicate); } if (StringUtils.isNotBlank(phone)) { //精确查询,equal Predicate predicate = cb.equal(root.get("phoneNumber").as(String.class), phone); predicates.add(predicate); } if (StringUtils.isNotBlank(accountType)) { Predicate predicate = cb.equal(root.get("accountType").as(String.class), accountType); predicates.add(predicate); } if (StringUtils.isNotBlank(city)) { Predicate predicate = cb.equal(root.get("city").as(String.class), city); predicates.add(predicate); } if (StringUtils.isNotBlank(type)) { Predicate predicate = cb.equal(root.get("type").as(String.class), type); predicates.add(predicate); } //判断是否有断言,如果没有则返回空,不进行条件组合 if (predicates.size() == 0) { return null; } //转换为数组,组合查询条件 Predicate[] p = new Predicate[predicates.size()]; return cb.and(predicates.toArray(p)); } }; //交给DAO处理查询任务 Page<User> dataPages = userDAO.findAll(specification, pageable);
多表多条件查询
在许多时候会面对多表多条件查询,实现实例如下
//封装查询对象Specification Specification<Courier> example = new Specification<Courier>() { @Override public Predicate toPredicate(Root<Courier> root, CriteriaQuery<?> query, CriteriaBuilder cb) { //获取客户端查询条件 String company = model.getCompany(); String courierNum = model.getCourierNum(); Standard standard = model.getStandard(); String type = model.getType(); //定义集合来确定Predicate[] 的长度,因为CriteriaBuilder的or方法需要传入的是断言数组 List<Predicate> predicates = new ArrayList<>(); //对客户端查询条件进行判断,并封装Predicate断言对象 if (StringUtils.isNotBlank(company)) { //root.get("company")获取字段名 //company客户端请求的字段值 //as(String.class)指定该字段的类型 Predicate predicate = cb.equal(root.get("company").as(String.class), company); predicates.add(predicate); } if (StringUtils.isNotBlank(courierNum)) { Predicate predicate = cb.equal(root.get("courierNum").as(String.class), courierNum); predicates.add(predicate); } if (StringUtils.isNotBlank(type)) { Predicate predicate = cb.equal(root.get("type").as(String.class), type); predicates.add(predicate); } //多表的条件查询封装,这是和单表查询的区别 if (standard != null) { if (StringUtils.isNotBlank(standard.getName())) { //创建关联对象(需要连接的另外一张表对象) //JoinType.INNER内连接(默认) //JoinType.LEFT左外连接 //JoinType.RIGHT右外连接 Join<Object, Object> join = root.join("standard",JoinType.INNER); //join.get("name")连接表字段值 Predicate predicate = cb.equal(join.get("name").as(String.class), standard.getName()); predicates.add(predicate); } } //判断结合中是否有数据 if (predicates.size() == 0) { return null; } //将集合转化为CriteriaBuilder所需要的Predicate[] Predicate[] predicateArr = new Predicate[predicates.size()]; predicateArr = predicates.toArray(predicateArr); // 返回所有获取的条件: 条件 or 条件 or 条件 or 条件 return cb.or(predicateArr); } }; //调用Dao方法进行条件查询 Page<Courier> page = courierDao.findAll(example, pageable);
Spring Data Jpa 简单模糊查询
在一些比较简单的查询条件下,不一定要使用 Specification 接口,比如
@Repository public interface UserRepository extends CrudRepository<User, Integer> { /** * username不支持模糊查询,deviceNames支持模糊查询 * @param deviceNames 模糊查询deviceNames * @param username 用户名称 * @return {@link List<User>} */ List<User> findAllByDeviceNamesContainingAndUsername(String deviceNames,String username); /** * 其中username不支持模糊查询,deviceNames支持模糊查询 * 传入的deviceNames需要在前后添加%,否则可能返回的结果是精确查询的结果 * @param deviceNames 模糊查询deviceNames * @param username 用户名称 * @return {@link List<User>} */ List<User> findAllByDeviceNamesLikeAndUsername(String deviceNames,String username); }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。