SpringDataJPA之Specification復(fù)雜查詢實戰(zhàn)
SpringDataJPA Specification復(fù)雜查詢
前言
繼上次SpringData-JPA之ExampleMatcher實例查詢使用一會之后發(fā)現(xiàn)ExampleMatcher對日期的查詢特別糟糕,所以才有了Specification查詢的研究。
- 20200114:更新對JpaSpecificationExecutor的解析,Specification思路2,以及CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery查詢部分
- 20180811:根據(jù)所學(xué)所用,重新更新了文章,并增加了Pageable分頁排序功能。
實現(xiàn)
對應(yīng)的Repository需要實現(xiàn)JpaSpecificationExecutor接口
public interface EventRepository extends JpaRepository<Event, Integer> , JpaSpecificationExecutor<Event>{
Specification與Controller業(yè)務(wù)邏輯
@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是接口實現(xiàn),new PageRequest()是舊方法,PageRequest.of()是新方法 //PageRequest.of的對象構(gòu)造函數(shù)有多個,page是頁數(shù),初始值是0,size是查詢結(jié)果的條數(shù),后兩個參數(shù)參考Sort對象的構(gòu)造方法 Pageable pageable = PageRequest.of(pageNumber,pageSize,Sort.Direction.DESC,"id"); //Specification查詢構(gòu)造器 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)); //這里可以設(shè)置任意條查詢條件 return null; //這種方式使用JPA的API設(shè)置了查詢條件,所以不需要再返回查詢條件Predicate給Spring Data Jpa,故最后return null } }; Page<Event> list=eventRepository.findAll(specification, pageable); return ApiReturnUtil.page(list); }
ApiReturnUtil.page封裝
其實這個大家根據(jù)自己的項目自己封裝,這部分不作為核心內(nèi)容,知識之前有部分網(wǎng)友比較糾結(jié)這個工具,所以簡單放出來參考一下.
public static ApiReturnObject page(Page returnObject) { return new ApiReturnObject(returnObject.getNumber()+"",returnObject.getNumberOfElements()+"",returnObject.getTotalElements()+"",returnObject.getTotalPages()+"","00","success",returnObject.getContent()); }
ApiReturnObject的主要內(nèi)容:
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等屬性,可對其進(jìn)行封裝
{ "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" }
可以查詢了。網(wǎng)上關(guān)于這個的資料也很少。希望可以幫到大家。
可能遇到的錯誤
Unable to locate Attribute with the the given name [event] on this ManagedType [org.microservice.tcbj.yytsg.checkcentersys.entity.Event]
出現(xiàn)這樣的情況,一般是因為實體類中沒有這個屬性,例如我Event的是eventTitle,寫成了event,就會報錯。
JpaSpecificationExecutor接口
20200114補充
JPA 提供動態(tài)接口JpaSpecificationExecutor,利用類型檢查的方式,利用Specification進(jìn)行復(fù)雜的條件查詢,比自己寫 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是我們傳入進(jìn)去的查詢參數(shù),是一個接口,并且只有一個方法
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); }
一個一目了然的方法
第二個實現(xiàn)思路:聽說這個方法已經(jīng)過時了,其實這個方法是最好理解的.這里附上作為思路參考.
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相關(guān)的CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery進(jìn)行查詢.
@PersistenceContext private EntityManager em; /** * CriteriaBuilder 安全查詢創(chuàng)建工廠,創(chuàng)建CriteriaQuery,創(chuàng)建查詢具體具體條件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<>(); //查詢條件設(shè)置 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進(jìn)行查詢 TypedQuery<Even> typedQuery = em.createQuery(query); return typedQuery.getResultList(); }
開發(fā)過程中JPA Specification的應(yīng)用
Specification算是JPA里面比較靈活的查詢規(guī)范了,方便實現(xiàn)復(fù)雜的查詢方式。
為什么需要Specification
Spring-Data JPA 本身支持了比較簡單的查詢方式,也就是根據(jù)屬性名成結(jié)合一些規(guī)范編寫查詢方法,例如,一個Customer對象有name屬性,那么如果想要實現(xiàn)根據(jù)name來查詢,只需要在接口文件中添加一個方法findByName(String name)即可實現(xiàn)。
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); }
但是在許多情況下,會有比較復(fù)雜的查詢,那么這個時候通過自動生成查詢方法的方式就不再可行。
應(yīng)用場景
為了實現(xiàn)復(fù)雜查詢,JPA提供了Criteria接口,這個是一套標(biāo)準(zhǔn)接口,來看一個例子,在一個平臺中,當(dāng)一個老客戶(注冊以來兩年)生日的時候,系統(tǒng)想要發(fā)送一個優(yōu)惠券給該用戶,那么傳統(tǒng)使用 JPA 2.0 Criteria API 去實現(xiàn):
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中查詢使用的實例
- 設(shè)置查詢條件,首先判斷今天是否為某個客戶的生日,然后判斷是否為老客戶
- 執(zhí)行查詢條件,獲得滿足條件的用戶
這里面的主要問題就是代碼擴展性比較差,因為需要設(shè)置CriteriaBuilder, CriteriaQuery, Root,同時這部分的代碼可讀性比較差。
JPA Specification實現(xiàn)復(fù)雜查詢
Specification為了實現(xiàn)可重用的斷言,JPA 里面引入了一個Specification接口,接口的封裝很簡單,如下
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }
在Java8中,我們可以非常方便地實現(xiàn)如上使用Criteria實現(xiàn)的效果
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)); }; } }
這樣對應(yīng)JPA的repository實現(xiàn)就可以如下
customerRepository.findAll(hasBirthday()); customerRepository.findAll(isLongTermCustomer());
而其實Specification為我們做的事情就是替我們準(zhǔn)備了CriteriaQuery, Root, CriteriaBuilder, 有了這些可重用的斷言之后,便可以將他們組合起來實現(xiàn)更加復(fù)雜的查詢了
customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));
JPA多條件、多表查詢
如果需要使用Specification,那么對應(yīng)的Repository需要實現(xiàn)接口JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {}
單表多條件查詢
在結(jié)合 Spring Boot 和 JPA 之后,為了四號線多條件查詢,并且整理分頁, 則可以考慮使用Predicate 斷言, 例如現(xiàn)在針對 User , 想要根據(jù)用戶的不同屬性進(jìn)行模糊查詢,同時如果屬性值為空或者空字符串,則跳過該屬性,不作為查詢條件,同時屬于單表多條件查詢,則
//在spring-jpa 2之后 不再使用 new PageRuest(page, pageSize) 的方式 Pageable pageable = PageRequest.of(page, pageSize); //實現(xiàn)條件查詢,組合查詢 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); } //判斷是否有斷言,如果沒有則返回空,不進(jìn)行條件組合 if (predicates.size() == 0) { return null; } //轉(zhuǎn)換為數(shù)組,組合查詢條件 Predicate[] p = new Predicate[predicates.size()]; return cb.and(predicates.toArray(p)); } }; //交給DAO處理查詢?nèi)蝿?wù) Page<User> dataPages = userDAO.findAll(specification, pageable);
多表多條件查詢
在許多時候會面對多表多條件查詢,實現(xiàn)實例如下
//封裝查詢對象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方法需要傳入的是斷言數(shù)組 List<Predicate> predicates = new ArrayList<>(); //對客戶端查詢條件進(jìn)行判斷,并封裝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); } //多表的條件查詢封裝,這是和單表查詢的區(qū)別 if (standard != null) { if (StringUtils.isNotBlank(standard.getName())) { //創(chuàng)建關(guān)聯(lián)對象(需要連接的另外一張表對象) //JoinType.INNER內(nèi)連接(默認(rèn)) //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); } } //判斷結(jié)合中是否有數(shù)據(jù) if (predicates.size() == 0) { return null; } //將集合轉(zhuǎn)化為CriteriaBuilder所需要的Predicate[] Predicate[] predicateArr = new Predicate[predicates.size()]; predicateArr = predicates.toArray(predicateArr); // 返回所有獲取的條件: 條件 or 條件 or 條件 or 條件 return cb.or(predicateArr); } }; //調(diào)用Dao方法進(jìn)行條件查詢 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需要在前后添加%,否則可能返回的結(jié)果是精確查詢的結(jié)果 * @param deviceNames 模糊查詢deviceNames * @param username 用戶名稱 * @return {@link List<User>} */ List<User> findAllByDeviceNamesLikeAndUsername(String deviceNames,String username); }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot使用ExceptionHandler做異常處理
這篇文章主要介紹了SpringBoot使用ExceptionHandler做異常處理,這篇文章通過多種方法案例來介紹該項技術(shù)的使用,需要的朋友可以參考下2021-06-06SpringBoot2.x 參數(shù)校驗問題小結(jié)
這篇文章主要介紹了SpringBoot2.x 參數(shù)校驗一些問題總結(jié),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08Java對象轉(zhuǎn)Json,關(guān)于@JSONField對象字段重命名和順序問題
這篇文章主要介紹了Java對象轉(zhuǎn)Json,關(guān)于@JSONField對象字段重命名和順序問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08SpringBoot3和ShardingSphere5框架實現(xiàn)數(shù)據(jù)分庫分表
這篇文章主要介紹了SpringBoot3和ShardingSphere5框架實現(xiàn)數(shù)據(jù)分庫分表的相關(guān)資料,需要的朋友可以參考下2023-08-08Spring中為bean指定InitMethod和DestroyMethod的執(zhí)行方法
在Spring中,那些組成應(yīng)用程序的主體及由Spring IoC容器所管理的對象,被稱之為bean,接下來通過本文給大家介紹Spring中為bean指定InitMethod和DestroyMethod的執(zhí)行方法,感興趣的朋友一起看看吧2021-11-11