当需要综合查询功能时,我们需要在仓库中继承JpaSpecificationExecutor接口。`specification`译为规范、说明书,`Executor`可译为执行者,所以`JpaSpecificationExecutor`可以译为Jpa规范的执行者,在此起到的是按JPA规范进行查询的作用。 repository/StudentRepository.java ``` package com.mengyunzhi.springBootStudy.repository; import com.mengyunzhi.springBootStudy.entity.Student; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; ★ import org.springframework.data.repository.PagingAndSortingRepository; /** * 学生 */ public interface StudentRepository extends PagingAndSortingRepository<Student, Long> , JpaSpecificationExecutor★ { } ``` > 与PagingAndSortingRepository不同,JpaSpecificationExecutor是个相对独立的接口,其并没有继承CrudRepository,所以如果只继承该接口将缺失基本的CRUD功能。 ![](https://img.kancloud.cn/ac/da/acda1dca2050077b5fb77cfa43593666_1008x456.png) 声明继承关系后,我们便可以使用如下方法进行综合查询了: ``` public interface JpaSpecificationExecutor<T①> { Optional<T> findOne(@Nullable Specification<T> var1); ➊ List<T> findAll(@Nullable Specification<T> var1);➋ Page<T> findAll(@Nullable Specification<T> var1, Pageable var2); ➌ List<T> findAll(@Nullable Specification<T> var1, Sort var2); ➍ long count(@Nullable Specification<T> var1); ➎ } ``` * ① 规定查询的实体 * ➊ 根据综合查询条件(查询规范)获取某一个实体 * ➋ 根据综合查询条件(查询规范)获取全部符合条件的实体 * ➌ 根据综合查询条件(查询规范)及分页条件,获取当前页实体及分页信息 * ➍ 根据综合查询条件(查询规范)及排序条件,获取全部符合条件的实体 * ➎ 根据综合查询条件(查询规范)获取符合条件的实体条数 spring官方提供了三种综合查询的方法。第一种方法使用简单,死板,适应能力差;第二、三种方法使相对复杂,但有更高的灵活性,适应能力强。 # 方法一:直接命名 JPA[官方文档](https://docs.spring.io/spring-data/jpa/docs/2.2.2.RELEASE/reference/html/#jpa.query-methods.query-creation)提供了一系列可以直接在StudentRepository中命名的方法,在前面的章节中我们使用了` List<Klass> findAllByNameContains(String name);` 完成了根据name查询班级信息。对于本例中的需求,我们可以在仓库中建立以下名称的方法: repository/StudentRepository.java ``` public interface StudentRepository extends PagingAndSortingRepository<Student, Long>, JpaSpecificationExecutor { Page<Student> findAllByNameContains➊AndSnoStartsWith➋AndKlass➌(String name①, String sno②, Klass klass③, Pageable pageable); } ``` * ➊ contains = 包含 * ➋ StartsWith = 开始于 * ①②③ 分别对应前面➊➋➌的查询要素 ## 测试 找到对应的测试文件:repository/StudentRepositoryTest.java,初始化如下: ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { /* 初始化2个班级并持久化*/ Klass klass = new Klass(); klass.setName("testKlass"); this.klassRepository.save(klass); Klass klass1 = new Klass(); klass1.setName("testKlass1"); this.klassRepository.save(klass1); Student student = new Student(); student.setName("testStudentName"); student.setSno("032282"); student.setKlass(klass); this.studentRepository.save(student); /* 初始化2个不同班级的学生并持久化 */ Student student1 = new Student(); student1.setName("testStudentName1"); student1.setSno("032291"); student1.setKlass(klass1); this.studentRepository.save(student1); } ``` 继续添加综合查询的断言 -- 加入三个查询条件,查询断言查询到了相关的数据: ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { ... this.studentRepository.save(student1); Page<Student> studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName", "032282", klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(1); } ``` ### name字段测试 断言:修改name参数的值,断言模糊查询生效: ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { ... studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStude", ① "032282", klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(1); studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "tStudentN", ② "032282", klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(1); studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "tudentName", ③ "032282", klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(1); studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName123", ④ "032282", klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(0④); } ``` * ① 截取前面部分字符 * ② 截取中间部分字符 * ③ 截取后台部分字符 * ④ 不在查询范围内(保证对name的查询是生效的) ### sno字段测试 ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { ... studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName", "03228",① klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(1); studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName", "3228",② klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(0); studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName", "32282", ③ klass, PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(0); } ``` * ① 截取前面部分关键字,生效 * ② 截取中间部分关键字,失效 * ③ 截取后面部分关键字,失效 ### 查询班级字段 ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { ... studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( "testStudentName", "032282", klass1, ① PageRequest.of(0, 2)); Assertions.assertThat(studentPage.getTotalElements()).isEqualTo(0); } ``` * ① 换为klass1,失效 ## 弊端 但该使用方法有一个非常大的弊端:那就是参数不能为null,比如我们如下使用: ``` @Test public void findAllByNameContainsAndSnoStartsWithAndKlass() { ... studentPage = this.studentRepository.findAllByNameContainsAndSnoStartsWithAndKlass( null★, "032282", klass1, PageRequest.of(0, 2)); } ``` 更会得到以下异常: ``` org.springframework.dao.InvalidDataAccessApiUsageException: Value must not be null!; nested exception is java.lang.IllegalArgumentException: Value must not be null! ``` 所以此方法被我们广泛的应用一些业务的逻辑处理中,而在综合查询中基本上所有的查询条件都**不**能是必须填写的,也就是说所有的查询条件都可能为null,此时使用null值进行查询时,此方法便会发生错误。也就是说本查询方法并不适用于当前的需求。在实际的生产环境中,此方法更多的被用于具有确认性的内部逻辑处理中。 **为与教程代码保持一致,请删除本方法涉及的功能及测试代码后继续学习** # 方法二:调用List<T> findAll(@Nullable Specification<T> var1) 我们当前的综合查询只需要调用findAll方法即可。比如在没有分页的情况下,我们调用`List<T> findAll(@Nullable Specification<T> var1)`。该方法中的参数var1的类型Specification是一个接口,其中T与其所在接口`JpaSpecificationExecutor<T>`的T相对应,表示一种约束。可以理解为:如果JpaSpecificationExecutor声明对某个表`T`进行操作,那么此规范也是必须是对某表`T`的规范。 该接口声明了一个方法: ``` public interface Specification { Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder); } ``` 如果想成功的调用findAll的方法,那么就必须创建一个实现了Specification接口的对象。创建该对象的方式有两种:1先创建一个实现了该接口的实现类,然后通过实例化该实现类的方法来得到对象;2 直接创建一个实现了该接口的对象。在此,我们直接使用第2种方法。 ## Specification 在使用JpaSpecificationExecutor进行综合查询以前,我们先使用单元测试的方法来了解下这个接口。打开test测试包下的repository/StudentRepositoryTest.java,然后新建specificationTest方法,并添加些测试代码\方法以进行一些初始化操作: ``` import javax.persistence.criteria.*; import java.util.List; ... private static final Logger logger = LoggerFactory.getLogger(StudentRepositoryTest.class); ... @Test public void specificationTest() { /* 初始化2个班级并持久化*/ Klass klass = new Klass(); klass.setName("testKlass"); this.klassRepository.save(klass); Klass klass1 = new Klass(); klass1.setName("testKlass1"); this.klassRepository.save(klass1); Student student = new Student(); student.setName("testStudentName"); student.setSno("032282"); student.setKlass(klass); this.studentRepository.save(student); /* 初始化2个不同班级的学生并持久化 */ Student student1 = new Student(); student1.setName("testStudentName1"); student1.setSno("032291"); student1.setKlass(klass1); this.studentRepository.save(student1); /* 断言存入了2个学生 */ List<Student> studentList = (List<Student>) this.studentRepository.findAll(); logger.info("当前数据库总计有{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(2); } ``` * 详见代码注释 ### 精确查询 假设我们想以班级一为查询条件进行查询,则需要实现Specification接口规范的对象如下: ``` ... logger.info("当前数据库总计有{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(2); /* 初始化查询条件对象 */ Specification<Student➊> klassSpecification = new Specification<Student➊>() { /** * 本条件查询:班级为klassEven的学生 * @param root 根(Student)实体 * @param criteriaQuery 条件查询 * @param criteriaBuilder 条件创建者 * @return 查询谓语 */ @Override public Predicate toPredicate(Root<Student➊> root, CriteriaQuery<?➋> criteriaQuery, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.equal➌(root.get("klass")➍, klass➎); } }; /* 加入班级条件后查询,断言查询的数量为1 */ studentList = (List<Student>) this.studentRepository.findAll(klassSpecification);➏ logger.info("使用班级为条件进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(1); } ``` * ➊ 对类型进行约束,大瓶子外面贴了什么标签,大瓶子中的小瓶子就必须也贴这个标签。 * ➋ `<? extends Object>`的缩写,表示容器中存的是任意类型的对象,**同时**该容器为只读容器,无法向该容器中添加新对象。 * ➌ 当➍和➎相`等于`时,满足查询条件。 * ➍ root在这代表student实体,root.get("klass")代表student实体上的klass属性 * ➎ 前面初始化的第一个班级 * ➏ 将`查询规范`做为条件传入findAll方法,即展开条件查询 测试结果符合预期 ``` 2019-11-27 14:56:33.662 INFO 51770 --- [ main] c.m.s.repository.StudentRepositoryTest : 当前数据库总计有2个学生 2019-11-27 14:56:33.740 INFO 51770 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用班级为条件进行查询,查询到1个学生 ``` ### like%查询 综合查询的接口说明了学号需要 以0322打头,断言查询到2条数据。 ``` Assertions.assertThat(studentList.size()).isEqualTo(1); /* 学号0322做为查询条件 */ Specification<Student> snoSpecification = new Specification<Student>() { @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.like(root.get("sno").as(String.class)➊, "0322%➋"); } }; /* 断言两个学生都是以此学号打头的 */ studentList = (List<Student>) this.studentRepository.findAll(snoSpecification); logger.info("使用学号0322条件进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(2); } ``` * ➊ 用as来指定该定段的类型(更严格),减少在运行中可能出现的异常 * ➋ 将`%`放后面,表示查询的结果必须以0322打头 以322打头,断言查询到0条数据, 以03228打头,断言查询到1条数据: ``` Assertions.assertThat(studentList.size()).isEqualTo(2); /* 学号以322打头 */ snoSpecification = new Specification<Student>() { @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.like(root.get("sno").as(String.class), "322%"); } }; /* 断言查询到0个学生 */ studentList = (List<Student>) this.studentRepository.findAll(snoSpecification); logger.info("使用学号322为条件进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(0); /* 学号03228打头,断言获取了一个学生 */ snoSpecification = new Specification<Student>() { @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { Root<Student> studentRoot = criteriaQuery.from(Student.class); return criteriaBuilder.like(root.get("sno").as(String.class), "03228%"); } }; studentList = (List<Student>) this.studentRepository.findAll(snoSpecification); logger.info("使用学号03228为条件进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(1); ``` 测试结果: ``` 2019-11-27 15:34:39.725 INFO 83358 --- [ main] c.m.s.repository.StudentRepositoryTest : 当前数据库总计有2个学生 2019-11-27 15:34:39.805 INFO 83358 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用班级为条件进行查询,查询到1个学生 2019-11-27 15:34:39.824 INFO 83358 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号0322条件进行查询,查询到2个学生 2019-11-27 15:34:39.835 INFO 83358 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号322为条件进行查询,查询到0个学生 2019-11-27 15:34:39.848 INFO 83358 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号03228为条件进行查询,查询到1个学生 ``` ### %like%查询 有了刚刚的基础,%like%查询便很简单了。 ``` logger.info("使用学号03228为条件进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(1); /* 姓名包含StudentN,断言获取了两个学生 */ Specification<Student> nameSpecification = new Specification<Student>() { @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.like(root.get("name").as(String.class), "%StudentN%"); } }; studentList = (List<Student>) this.studentRepository.findAll(nameSpecification); logger.info("姓名包含StudentN进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(2); /* 姓名包含Name1,断言获取了1个学生 */ nameSpecification = new Specification<Student>() { @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.like(root.get("name").as(String.class), "%Name1%"); } }; studentList = (List<Student>) this.studentRepository.findAll(nameSpecification); logger.info("姓名包含Name1进行查询,查询到{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(1); } ``` 测试结果: ``` 2019-11-27 16:15:23.157 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 当前数据库总计有2个学生 2019-11-27 16:15:23.237 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用班级为条件进行查询,查询到1个学生 2019-11-27 16:15:23.257 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号0322条件进行查询,查询到2个学生 2019-11-27 16:15:23.268 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号322为条件进行查询,查询到0个学生 2019-11-27 16:15:23.281 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 使用学号03228为条件进行查询,查询到1个学生 2019-11-27 16:15:23.304 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 姓名包含StudentN进行查询,查询到2个学生 2019-11-27 16:15:23.321 INFO 17360 --- [ main] c.m.s.repository.StudentRepositoryTest : 姓名包含Name1进行查询,查询到1个学生 ``` 至此,我们了解了spring data jpa的综合查询方法中的equeal查询及like查询,更多综合查询的方法我们可以参考Hibernate(Spring Data JPA内置了Hibernate)的官方文档。 # 全局测试 最后我们对整体的项目进行单元测试,以避免本小节中的代码对其已有功能造成影响 。 ![](https://img.kancloud.cn/48/77/4877f4e5ef7a5cadfa9a68ca21125e1d_738x284.png) 这是由于在进行整个项目的单元测试时,单元测试间互相进行了影响。其根本原因在于我们的错误假设:我们假设在对某个方法进行单元测试时,数据库中是没有数据的。如果不存在其它的单元测试或是不对整个项目进行整体测试,这个理论是正确的。但一旦cf 整个项目进行单元测试,那么就有可能出现:A测试在测试中生成了一些冗余数据,而这些数据对B测试造成了影响 。比如上图,我们期望得到2条数据,最终却得到了5条。就是由于其它的单元测试进行测试时,生成3条。为了避免单元测试的互相影响,有两种解决方案:1是在每次测试后,手机的回滚数据;2是在每个单元测试中均假设此单元测试是在一个**非**空数据库下进行的。在实际的使用中,我们发现第2种解决方案更简单一些,在此我们也采用第二种,对报错的方法修正如下: repository/StudentRepository.java ``` @Test public void specificationTest() { List<Student> oldStudentList = (List<Student>) this.studentRepository.findAll(); ① ... logger.info("当前数据库总计有{}个学生", studentList.size()); Assertions.assertThat(studentList.size()).isEqualTo(2 + oldStudentList.size()); ② ``` * ① 获取当前数据库中的历史记录 * ② 断言新增了2条,而非共2条 除此以外,我们在单元测试中使用了静态的“用户名”、“姓名”也是不对的,这极可能给以后的测试埋下地雷。在它暂不做修正,如果后续踩雷的话,我们再回头看。 # <? extends Obejct> 在上述查询中,我们并没有使用到`CriteriaQuery<?> criteriaQuery`,在生产项目中我们也很少使用该参数,对于其具体的功能笔者不了解所以也无法讲述,但`<?>`这种用法是第一次出现,在这简单用实例进行说明: `<?>`是`<? extends Object>`的简写形式,表示:该容器中的对象继承(或实现)了`Object`,所以只要`Object`中声明的方法,我们都可以在此对象上调用。但是:由于编译器无法确定该容器中所包含对象的具体类型,因而我们无法向该容器中写入新的对象,所以该容器只能是只读的。比如有如下代码: ``` private void test() { List<Object> lists = new ArrayList<>(); ① lists.add("test"); ① lists.add(123); ① lists.add(true); ① List<? extends Object➊> lists1 = Arrays.asList(123, 456); ➋ Object index0 = lists1.get(0); ➌ lists1.add(new Object()); ➍ lists1.add("test"); ➍ lists1.add(123); ➍ lists1.add(true); ➍ } ``` * ① 编译通过,正常执行 * ➋ 中`123`,`456`的类型均继承了➊规定的Object,编译通过 * ➌ lists1中元素可读,编译通过,获取的类型为Object。 * ➍ lists1不可写入新元素,编译**失败** JAVA为什么要这样做呢?可以在《Head First Java》的570页附近找到答案,比如Cat和Dog类都实现了Animal接口,如果允许修改容器中的值,那么以下代码在编译过程中是可以通过的。 ``` public void go() { ① LIst<Dog> dogs = Arrays.asList(new Dog(1), new Dog(2)); takeAnimals(dogs); Dog dog = dogs.get(0); ➋ } public void takeAnimals(List<? extends Animals> animals) { ② animals.set(0, new Cat(1)); ➊ } ``` 观察得知方法①②(假设animals可写)的代码都是没有问题的。但在运行时,由于➊将第0个元素修改为了Cat,所以在➋中想获取Dog时便会发生错误。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | spring jpa query creation| [https://docs.spring.io/spring-data/jpa/docs/2.2.2.RELEASE/reference/html/#jpa.query-methods.query-creation](https://docs.spring.io/spring-data/jpa/docs/2.2.2.RELEASE/reference/html/#jpa.query-methods.query-creation) | 10 | | Hibernate userguide | [https://docs.jboss.org/hibernate/orm/current/userguide/html\_single/Hibernate\_User\_Guide.html#criteria-from-root](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#criteria-from-root) | - | | spring data jpa | [https://docs.spring.io/spring-data/jpa/docs/2.2.2.RELEASE/reference/html/#specifications](https://docs.spring.io/spring-data/jpa/docs/2.2.2.RELEASE/reference/html/#specifications) | 10 | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.3) | - |