为何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,经过自动转换,省去了手动查询的过程。

相关文章
相关标签/搜索