前言
在之前的文章Spring中CriteriaBuilder.In<T>的使用中,留下了一个悬念:
- 为什么Spring Data JPA中的Specification类型可以兼容criteriaBuilder生成的Predicate查询条件?
方法
在开始之前,先展示IDEA中两个基本操作
查看文件位置
选中某个类的类名,按Option + F1,可以在文件目录中显示这个文件。
在文件列表中可以看到:Java综合查询所用到的内部类实际上都在持久层(persistence)的criteria包中,常见的Root, criteriaBuilder, criteriaQuery都在其中。
查看继承关系
选中某个类的类名,在右键菜单中,选择"Show Diagram Popup"。
即可显示继承关系:
用以上的方法,可以汇总出一张继承关系图。
继承关系图
一开始我猜测:
criteriaBuilder生成的Predicate,和JPA中的Specification 继承于同一个接口,因此互相兼容。所以我需要找到这两个接口的继承关系图。
criteria
把javax.persistence.criteria包中所有的接口绘制到一张继承关系图上,如下:
这张图过于复杂了,接下来简化一下,只保留于builder,相关的接口。
到此已经能解决一部分问题:由于in实现了Predicate,Predicate实现了Expression,因此这三者之间是相互兼容的,如果方法接收Expression,那么返回子类的对象也是可以的。
Specification
主角登场。
不过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);
}
可以看到:
- 项目中,在new Specification接口时,实现了其中的一个方法,方法中写的就是“其他查询条件”,此时返回的是predicate。
- 在下面的代码中,又把另一个查询条件添加到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的拼接。
代码执行顺序简图
总结
之所以Spring Data JPA中的Specification类型可以兼容criteriaBuilder生成的Predicate条件,是因为Specification接口中内置了一个toPredicate()方法。
所有的查询条件,实际上都是在重写toPredicate()方法。
Specification的拼接,实际上是在拼接builder。
查询的过程中,经过一系列调用,关键步骤在于,把Specification转换成了predicate,
由于之前进行了拼接,此时会把各部分的toPredicate()方法都执行一次,于是就加入了所有的查询条件。
最终仓库执行查询时,其实就是按照原生Java的查询来操作的,只不过由于使用了Specification,通过自动转换,省去了手动查询的过程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。