当需要综合查询功能时,我们需要在仓库中继承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) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用