6

前言

在之前的文章Spring中CriteriaBuilder.In<T>的使用中,留下了一个悬念:

  • 为什么Spring Data JPA中的Specification类型可以兼容criteriaBuilder生成的Predicate查询条件?

方法

在开始之前,先展示IDEA中两个基本操作

查看文件位置

选中某个类的类名,按Option + F1,可以在文件目录中显示这个文件。
image.png

在文件列表中可以看到:Java综合查询所用到的内部类实际上都在持久层(persistence)的criteria包中,常见的Root, criteriaBuilder, criteriaQuery都在其中。

查看继承关系

选中某个类的类名,在右键菜单中,选择"Show Diagram Popup"。
image.png
即可显示继承关系:
image.png

用以上的方法,可以汇总出一张继承关系图。

继承关系图

一开始我猜测:

criteriaBuilder生成的Predicate,和JPA中的Specification 继承于同一个接口,因此互相兼容。所以我需要找到这两个接口的继承关系图。

criteria

javax.persistence.criteria包中所有的接口绘制到一张继承关系图上,如下:

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


LYX6666
1.6k 声望75 粉丝

一个正在茁壮成长的零基础小白