在以前的文章Spring中CriteriaBuilder.In<T>的使用中,留下了一個懸念:java
在開始以前,先展現IDEA中兩個基本操做segmentfault
選中某個類的類名,按Option + F1,能夠在文件目錄中顯示這個文件。
app
在文件列表中能夠看到:Java綜合查詢所用到的內部類實際上都在持久層(persistence)的criteria包中,常見的Root, criteriaBuilder, criteriaQuery都在其中。dom
選中某個類的類名,在右鍵菜單中,選擇"Show Diagram Popup"。
便可顯示繼承關係:
ide
用以上的方法,能夠彙總出一張繼承關係圖。源碼分析
一開始我猜想:ui
criteriaBuilder生成的Predicate,和JPA中的Specification 繼承於同一個接口,所以互相兼容。因此我須要找到這兩個接口的繼承關係圖。this
把javax.persistence.criteria包中全部的接口繪製到一張繼承關係圖上,以下:spa
這張圖過於複雜了,接下來簡化一下,只保留於builder,相關的接口。翻譯
到此已經能解決一部分問題:因爲in實現了Predicate,Predicate實現了Expression,所以這三者之間是相互兼容的,若是方法接收Expression,那麼返回子類的對象也是能夠的。
主角登場。
不過Specification的繼承關係很簡單,以下:
僅從圖片上看,彷佛Specification和Predicate毫無關聯,因而我陷入了思考。忽然想到了項目中的一個細節:
有一段代碼,在new Specification對象時,實現了一個名爲toPredicate的方法,可是方法裏寫的是其餘的查詢條件:
@Override public Predicate toPredicate(Root<Subject> root, CriteriaQuery<?> query, CriteriaBuilder builder) { logger.debug("校驗當前登陸用戶專業課信息"); User user = authService.getCurrentLoginUser(); logger.debug("parent 爲 null 查詢條件"); Predicate predicate = root.get("parent").isNull(); Predicate userPredict = builder.equal(root.join("createUser").get("id"), user.getId()); predicate = builder.and(predicate, userPredict); logger.debug("構造課程查詢條件"); if (courseId != null) { Predicate coursePredicate = builder.equal(root.join("course").get("id").as(Long.class), courseId); predicate = builder.and(predicate, coursePredicate); } return predicate; }
問題在於,並無其餘任何地方調用了這個方法,那麼爲何要實現這個方法那?這些代碼怎麼被添加到查詢條件中呢?
接下來我猜想,既然Specification和Predicate能夠互相兼容,那麼必定存在一個方法能完成兩者之間的轉換。
而且,既然沒有手動調用這個方法,那麼它應該是在內部類中被調用了。
接下來開始找源碼:
@Override public Page<Subject> page(Pageable pageable, Long courseId, Long modelId, Integer difficult, List<Long> tagIds) { Specification<Subject> specification = new Specification<Subject>() { @Override public Predicate toPredicate(Root<Subject> root, CriteriaQuery<?> query, CriteriaBuilder builder) { logger.debug("校驗當前登陸用戶專業課信息"); User user = authService.getCurrentLoginUser(); logger.debug("parent 爲 null 查詢條件"); Predicate predicate = root.get("parent").isNull(); Predicate userPredict = builder.equal(root.join("createUser").get("id"), user.getId()); predicate = builder.and(predicate, userPredict); logger.debug("構造課程查詢條件"); if (courseId != null) { Predicate coursePredicate = builder.equal(root.join("course").get("id").as(Long.class), courseId); predicate = builder.and(predicate, coursePredicate); } return predicate; } }; specification = specification.and(SubjectSpecs.issuedCourses(userService.getCurrentLoginUser().getCourses())); return this.subjectRepository.findAll(specification, pageable); }
能夠看到:
在1和2中,是兩種不一樣的對象,而且它們之間並無聯繫。
所以,這兩種查詢條件,多是在findAll執行以後,才完成轉化和拼接的。
查看倉庫接口的實現類中findAll的源碼,以下:
@Override public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) { TypedQuery<T> query = getQuery(spec, pageable); return isUnpaged(pageable) ? new PageImpl<T>(query.getResultList()) : readPage(query, getDomainClass(), pageable, spec); }
傳入了Specification和分頁,調用了getQuery(),在getQuery()中傳入了Specification。
因此咱們繼續跟蹤。
protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Pageable pageable) { Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted(); return getQuery(spec, getDomainClass(), sort); }
傳入了Specification和分頁,而後進行判斷,若是分頁正常,就把分頁轉換成Sort,而後調用getquery()的重載方法。
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<S> query = builder.createQuery(domainClass); // 標記:這裏是關鍵 Root<S> root = applySpecificationToCriteria(spec, domainClass, query); query.select(root); if (sort.isSorted()) { query.orderBy(toOrders(sort, root, builder)); } return applyRepositoryMethodMetadata(em.createQuery(query)); }
到此已經開始像原生查詢同樣,構建root, buider, query等對象了。
代碼的標記處,調用了一個applySpecificationToCriteria方法,翻譯過來就是本文標題了!
看到這我彷佛已經找到了答案。繼續追蹤。
private <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec, Class<U> domainClass, CriteriaQuery<S> query) { Assert.notNull(domainClass, "Domain class must not be null!"); Assert.notNull(query, "CriteriaQuery must not be null!"); Root<U> root = query.from(domainClass); if (spec == null) { return root; } CriteriaBuilder builder = em.getCriteriaBuilder(); // 標記:關鍵點 Predicate predicate = spec.toPredicate(root, query, builder); if (predicate != null) { query.where(predicate); } return root; }
終於看到了對toPredicate()方法的調用!
說明一開始的猜測是正確的。
因爲咱們重寫了toPredicate()方法,所以會按照代碼,把其餘查詢條件添加進去。
至此,主要問題已解決,獲取query以後逐層返回,完成查詢。
此時還有一個小疑惑,從始至終並無看到拼接兩個條件的代碼,那麼拼接過程是什麼時候進行的呢?
這時就要提到.and方法。
@Nullable default Specification<T> and(@Nullable Specification<T> other) { return composed(this, other, (builder, left, rhs) -> builder.and(left, rhs)); }
傳入了三個參數,this(當前Specification), other(須要拼接的Specification), 拼接方法。所以,Specification的拼接實質上也是buider的拼接。
之因此Spring Data JPA中的Specification類型能夠兼容criteriaBuilder生成的Predicate條件,是由於Specification接口中內置了一個toPredicate()方法。
全部的查詢條件,實際上都是在重寫toPredicate()方法。
Specification的拼接,其實是在拼接builder。
查詢的過程當中,通過一系列調用,關鍵步驟在於,把Specification轉換成了predicate,
因爲以前進行了拼接,此時會把各部分的toPredicate()方法都執行一次,因而就加入了全部的查詢條件。
最終倉庫執行查詢時,其實就是按照原生Java的查詢來操做的,只不過因爲使用了Specification,經過自動轉換,省去了手動查詢的過程。