在以前的文章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,经过自动转换,省去了手动查询的过程。