爲何SpringDataJPA中的Specification類型能夠兼容criteriaBuilder生成的Predicate

前言

在以前的文章Spring中CriteriaBuilder.In<T>的使用中,留下了一個懸念:java

  • 爲何Spring Data JPA中的Specification類型能夠兼容criteriaBuilder生成的Predicate查詢條件?

方法

在開始以前,先展現IDEA中兩個基本操做segmentfault

查看文件位置

選中某個類的類名,按Option + F1,能夠在文件目錄中顯示這個文件。
image.pngapp

在文件列表中能夠看到:Java綜合查詢所用到的內部類實際上都在持久層(persistence)的criteria包中,常見的Root, criteriaBuilder, criteriaQuery都在其中。dom

查看繼承關係

選中某個類的類名,在右鍵菜單中,選擇"Show Diagram Popup"。
image.png
便可顯示繼承關係:
image.png ide

用以上的方法,能夠彙總出一張繼承關係圖。源碼分析

繼承關係圖

一開始我猜想:ui

criteriaBuilder生成的Predicate,和JPA中的Specification 繼承於同一個接口,所以互相兼容。因此我須要找到這兩個接口的繼承關係圖。this

criteria

javax.persistence.criteria包中全部的接口繪製到一張繼承關係圖上,以下:spa

Java Presistance-Criteria.png

這張圖過於複雜了,接下來簡化一下,只保留於builder,相關的接口。翻譯

image.png

到此已經能解決一部分問題:因爲in實現了PredicatePredicate實現了Expression,所以這三者之間是相互兼容的,若是方法接收Expression,那麼返回子類的對象也是能夠的。

Specification

主角登場。
不過Specification的繼承關係很簡單,以下:

image.png

小結

僅從圖片上看,彷佛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. 項目中,在new Specification接口時,實現了其中的一個方法,方法中寫的就是「其餘查詢條件」,此時返回的是predicate。
  2. 在下面的代碼中,又把另外一個查詢條件添加到Specification中,調用了倉庫的findAll方法,傳入了這個Specification。

在1和2中,是兩種不一樣的對象,而且它們之間並無聯繫。

所以,這兩種查詢條件,多是在findAll執行以後,才完成轉化和拼接的。

findAll(Specification, pageable)

查看倉庫接口的實現類中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。

因此咱們繼續跟蹤。

getQuery(Specification, pageable)

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()的重載方法。

getQuery(Specification, domainClass, sort)

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方法,翻譯過來就是本文標題了!

看到這我彷佛已經找到了答案。繼續追蹤。

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以後逐層返回,完成查詢。

Specification.and()

此時還有一個小疑惑,從始至終並無看到拼接兩個條件的代碼,那麼拼接過程是什麼時候進行的呢?
這時就要提到.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的拼接。

代碼執行順序簡圖

Java Presistance-執行順序.png

總結

之因此Spring Data JPA中的Specification類型能夠兼容criteriaBuilder生成的Predicate條件,是由於Specification接口中內置了一個toPredicate()方法。

全部的查詢條件,實際上都是在重寫toPredicate()方法。

Specification的拼接,其實是在拼接builder。

查詢的過程當中,通過一系列調用,關鍵步驟在於,把Specification轉換成了predicate,

因爲以前進行了拼接,此時會把各部分的toPredicate()方法都執行一次,因而就加入了全部的查詢條件。

最終倉庫執行查詢時,其實就是按照原生Java的查詢來操做的,只不過因爲使用了Specification,經過自動轉換,省去了手動查詢的過程。

相關文章
相關標籤/搜索