Spring Data JPA 原理与实战第十四天 Spring Data Rest和Spring Data ElasticSearch

30 Spring Data Rest 是什么?和 JPA 是什么关系?

通过之前课时的内容,相信你已经对 JPA 有了深入的认识了,那么 JPA 还有哪些应用场景呢?这一讲,我们将通过 `Spring Data Rest` 来聊聊实体和 Respository 的另外一种用法。

首先通过一个 Demo 让你感受一下,怎么快速创建一个 Rest 风格的 Server 服务端。

Spring Data Rest Demo

我们通过以下四个步骤演示一下 Spring Data Rest 的效果。

第一步:通过 gradle 引入相关的 jar 依赖,代码如下所示。

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// spring data rest的依赖,由于我们使用的是spring boot,所以只需要添加starter即可
implementation("org.springframework.boot:spring-boot-starter-data-rest")
//我们添加swagger方便看得出来,生成了哪些api接口
implementation 'io.springfox:springfox-boot-starter:3.0.0'
// swagger 对spring data reset支持需要添加 springfox-data-rest
implementation 'io.springfox:springfox-data-rest:3.0.0'

添加完依赖之后,我们可以通过 gradle 的依赖视图看一下都用了哪些 jar 包。

Drawing 0.png

通过上图可以很清晰地看到 spring-data-rest 的 jar 包引入情况,以及我们依赖的 spring-data-jpa 和 Swagger。

第二步:在项目里面添加 SpringFoxConfiguration 开启 Swagger,代码如下所示。

@Configuration
@EnableSwagger2
public class SpringFoxConfiguration {}

第三步:通过 application.properties 指定一个 base-path,以方便和我们自己的 api 进行区分,代码如下所示。

# 我们可以通过spring data rest里面提供的配置项,指定bast-path
spring.data.rest.base-path=api/rest/v2

第四步:直接启动项目,就可以看到效果了,不需要任何额外的 controller 的配置和设置

启动成功之后,我们就会发现,里面多了很多 api/rest/v2 等 Rest 风格的 API,并且可以直接使用。如下图所示,不只有我们自己的 Controller,还有 Spring DataRest 自己生成的 API。

Drawing 1.png

这时我们打开 Swagger 看一下:http://127.0.0.1:8087/swagger-ui/

Drawing 2.png

由于我们的 Demo 的项目结构是下图所示这样的。

Drawing 3.png

你会发现有几个 Repository 会帮我们生成几个对应的 Rest 协议的 API,除了基本的 CRUD,例如 UserInfoRespository 自定义的方法它们也会帮我们展示出来。而 Room 实体我们没有对应的 Repository,所以不会有对应的 Rest 风格 API 生成。

通过这个 Demo 你可以想象一下,如果要做一个 Rest 风格的 Server API 项目,是不是只需要把对应的 Entity 和 Repository 建好,就可以直接拥有了所有的 CRUD 的 API 了?这样可以大大提高我们的开发效率。

下面我们详细看一下 Spring Data Rest 的基本用法。

Spring Data Rest 基本用法

通过 Demo 可以看得出来,Spring Data Rest 的核心功能就是把 Spring Data Resositories 里对外暴露的方法生成对应的 API,如我们上面的 AddressRepository,里面对应的实体是 Address,代码如下。

public interface AddressRepository extends JpaRepository<Address, Long>{
}

它帮我们生成的 API 有下图所示的这些。

Drawing 4.png

从 swagger 我们可以看到 Spring Data Rest 的几点用法。

语义化的方法

把实体转化成复数的形式,生成基本的 PATCH、GET、PUT、POST、DELETE 带有语义的 Rest 相应的方法,包括的子资源有如下几个。

  • GET:返回单个实体

  • PUT:更新资源

  • PATCH:与 PUT 类似,但部分是更新资源状态

  • DELETE:删除暴露的资源

  • POST:从给定的请求正文创建一个新的实体

默认的状态码的支持
  • 200 OK:适用于纯粹的 GET 请求

  • 201 Created:针对创建新资源的 POST 和 PUT 请求

  • 204 No Content:对于 PUT、PATCH 和 DELETE 请求

  • 401 没有认证

  • 403 没有权限,拒绝访问

  • 404 没有找到对应的资源

分页支持

通过 Swagger,我们可以看到其完全对分页和排序进行支持,完全兼容我们之前讲过的 Spring Data JPA 的分页和排序的参数,如下图所示。

Drawing 5.png

通过 @RepositoryRestResource 改变资源的 metaData

代码如下所示。

@RepositoryRestResource(
      exported = true, //资源是否暴露,默认true
      path = "users",//资源暴露的path访问路径,默认实体名字+s
      collectionResourceRel = "userInfo",//资源名字,默认实体名字
      collectionResourceDescription = @Description("用户基本信息资源"),//资源描述
      itemResourceRel = "userDetail",//取资源详情的Item名字
      itemResourceDescription = @Description("用户详情")
)

我们将其放置在 UserInfoRepository 上面测试一下,代码变更如下。

@RepositoryRestResource(
      exported = true,
      path = "users",
      collectionResourceRel = "userInfo",
      collectionResourceDescription = @Description("用户资源"),
      itemResourceRel = "userDetail",
      itemResourceDescription = @Description("用户详情")
)
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {}

这时通过 Swagger 可以看到,url 的 path 上面变成了 users,而 body 里面的资源名字变成了 userInfo,取 itemResource 的 URL 描述变成了 userDetail,如下图所示。

Drawing 6.png

@RepositoryRestResource 是使用在 Repository 类上面的全局设置,我们也可以针对具体的 Repsitory 里面的每个方法进行单独设置,这就是另外一个注解:@RestResource。

@RestResource 改变 rest 的 SearchPath

代码如下所示。

@RestResource(
      exported = true,//是否暴露给Search
      path = "findCities",//Search后面的path路径
      rel = "cities"//资源名字
)

可以将其用于 ***Repository 的方法中和 @Entity 的实体关系上,那么我们在 address 的 findByAddress 方法上面做一个测试,看看会变成什么样,代码如下所示。

public interface AddressRepository extends JpaRepository<Address, Long>{
    @RestResource(
            exported = true,//是否暴露给Search
            path = "findCities",//Search后面的path路径
            rel = "cities"//资源名字
    )
    Page<Address> findByAddress(@Param("address") String address, Pageable pageable);
}

我们打开 Swagger 看一下结果,会发现 search 后面的 path 路径被自定义了,如下图所示。

Drawing 7.png

同时这个注解也可以配置在关联关系上,如 @OneToMany 等。如果我们不想某些方法暴露成 RestAPI,就直接添加 @RestResource(exported = false) 这一注解即可,例如一些删除方法等。

spring data rest 的配置项支持

这个可以直接在 application.properties 里面配置,我们在 IDEA 里面输入前缀的时候,就会有如下提示。

Drawing 8.png

对应的描述如下表所示。

Lark20201224-161329.png

Spring Data Rest 的常见用法我们介绍完了,之前还讲过 Spring Data JPA 对 Jackson 的支持,它在 Spring Data Rest 里面完全适用,下面来看一下。

返回结果对 Jackson 的支持

通过 jackson 的注解,可以改变 rest api 的属性的名字,或者忽略具体的某个属性。我们在 address 的实体里面,改变一下属性 city 的名字,同时忽略 address 属性,代码会变成如下所示的样子。

@Entity
@Table
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "userInfo")
public class Address extends BaseEntity {
   @JsonProperty("myCity") //改变JSON响应的属性名字
   private String city;
   @JsonIgnore //JSON解析的时候忽略某个属性
   private String address;
}

我们通过 Swagger 里面的 Description 可以看到,当前的资源的描述发生了变化,字段名变成了 myCity,address 属性没有了,具体如下图所示。

Drawing 9.png

Spring Data Rest 返回 ResponseBody 的原理和接收 RequestBody 的原理都是基于 JSON 格式的,我们之前讲的 Jackson 的所有注解语法同样适用。

那么介绍了这么多,到底 Spring Data Rest 和 Spring Data JPA 是什么关系呢?我们来总结一下。

Spring Data Rest 和 Spring Data JPA 的关系

大概有如下几点。

  1. Spring Data JPA 基于 JPA 协议提供了一套标准的 Repository 的操作统一接口,方法名和 @Query 都是有固定语法和约定的规则的。

  2. Spring Data Rest 利用 JPA 的约定和语法,利用 Java 反射、动态代理等机制,很容易可以生成一套标准的 rest 风格的 API 协议操作。

  3. 也就是说 JPA 制定协议和标准,Spring Data Rest 基于这套协议生成 rest 风格的 Controller。

总结

由于篇幅有限,SpringDataRest 本身的原理和实现方式一个课时是介绍不完的,虽然这一讲的内容不多,但其精髓都在这里了。

我想表达的重点是 JPA 的应用领域其实有很多,我的讲解就是想帮你打开思路,在写一些基于实体的框架时就可以参考 Spring Data Rest 的做法。例如yahoo 团队设计的 JSONAPI 协议,以及Elide 的实现,也是基于 JPA 的实体注解来实现的。

甚至 Spring 在研究的 graph QL,也可以基于约定的实体来做很多事情。所以当你把 JPA “玩得很溜”的时候,就可以大大提升开发效率。

最后欢迎你在留言区发表自己的看法,希望我们可以一起讨论。下一讲我们来聊聊如何通过 spring boot test 提高开发效率,到时见。


31 如何利用单元测试和集成测试让你开发效率翻倍?

在实际工作中,我发现有些开发人员不喜欢写测试用例,感觉是在浪费时间,但是要知道,如果我们测试用例非常完备,是可以提升团队体效率的。那么这一讲我们就针对这一问题,来看看如何使用单元测试,以及如何快速地写单元测试。

由于工作中常见的有 junit 4、junit 5 两个版本,我们使用的是 Spring boot 2.1,里面默认集成了 junit5,所以今天我们以 junit 5 进行讲解。我们先从数据库层开始。

Spring Data JPA 单元测试的最佳实践

实际工作中我们免不了要和 Repository 打交道,那么这层的测试用例应该怎么写呢?怎么才能提高开发效率呢?关于 JPA 的 Repository,下面我们分成两个部分来介绍:了解基本语法;分析最佳实践。

Spring Data JPA Repository 的测试用例

测试用例写法步骤如下。

第一步:引入 test 的依赖,gradle 的语法如下所示。

testImplementation 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

第二步:利用项目里面的实体和 Repository,假设我们项目里面有 Address 和 AddressRepository,代码如下所示。

@Entity
@Table
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class Address extends BaseEntity {
   private String city;
   private String address;
}
//Repository的DAO层
public interface AddressRepository extends JpaRepository<Address, Long>{

}

第三步:新建 RepsitoryTest,@DataJpaTest 即可,代码如下所示。

@DataJpaTest
public class AddressRepositoryTest {
    @Autowired
    private AddressRepository addressRepository;
    //测试一下保存和查询
    @Test
    public  void testSave() {
        Address address = Address.builder().city("shanghai").build();
        addressRepository.save(address);
        List<Address> address1 = addressRepository.findAll();
        address1.stream().forEach(address2 -> System.out.println(address2));
    }
}

通过上面的测试用例可以看到,我们直接添加了 @DataJpaTest 注解,然后利用 Spring 的注解 @Autowired,引入了 spring context 里面管理的 AddressRepository 实例。换句话说,我们在这里面使用了集成测试,即直接连接的数据库来完成操作。

第四步:直接运行上面的测试用例,可以得到如下图所示的结果。

Drawing 0.png

通过测试结果,我们可以发现:

  1. 我们的测试方法默认都会开启一个事务,测试完了之后就会进行回滚;

  2. 里面执行了 insert 和 select 两种操作;

  3. 如果开启了 Session Metrics 的日志的话,也可以观察出来其发生了一次 connection。

通过这个案例,我们可以知道 Repository 的测试用例写起来还是比较简单的,其中主要利用了 @DataJpaTest 的注解。下面我们打开 @DataJpaTest 的源码,看一下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class) //测试环境的启动方式
@ExtendWith(SpringExtension.class)//加载了Spring测试环境
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa//加载了依赖Spring Data JPA的原有配置
@AutoConfigureTestDatabase //加载默认的测试数据库,我们这里面采用默认的H2
@AutoConfigureTestEntityManager//加载测试所需要的EntityManager,主要是事务处理机制不一样
@ImportAutoConfiguration
public @interface DataJpaTest {
   //默认打开sql的控制台输出,所以当我们什么都没有做的时候就可以看到SQL了
   @PropertyMapping("spring.jpa.show-sql")
   boolean showSql() default true;
......}

通过源码会发现 @DataJpaTest 注解帮我们做了很多事情:

  1. 加载 Spring Data JPA 所需要的上下文,即数据库,所有的 Repository;

  2. 启用默认集成数据库 h2,完成集成测试。

现在我们知道了 @DataJpaTest 所具备的能力,那么在实际工作中,哪些场景会需要写 Repository 的测试用例呢?

Repository 的测试场景

可能在工作中,有的同事会说没有必要写 Repository 的测试用例,因为好多方法都是框架里面提供的,况且这个东西没有什么逻辑,写的时候有点浪费时间。

其实不然,如果能把 Repository 的测试用写好的话,这对我们的开发效率绝对是有提高的。否则当给你一个项目,让你直接改里面的代码,你可能就会比较慌,不敢改。所有你就要知道都有哪些场景我们必须要写 Repository 的测试用例。

场景一:当新增一个 Entity 和实体对应的 Repository 的时候,需要写个简单的 save 和查询测试用例,主要目的是检查我们的实体配置是否正确,否则当你写了一大堆 Repository 和 Entity 的时候,启动报错,你就傻眼了,不知道哪里配置得有问题,这样反而会降低我们的开发效率;

场景二:当实体里面有一些 POJO 的逻辑,或者某些字段必须要有的时候,我们就需要写一些测试用例,假设我们的 Address 实体里面不需要有 address 属性字段,并且有一个 @Transient 的字段和计算逻辑,如下述代码所示。

public class Address extends BaseEntity {
   @JsonProperty("myCity")
   private String city;
   private String address; //必要字段
   @Transient //非数据库字段,有一些简单运算
   private String addressAndCity;
   public String getAddressAndCity() {
      return address+"一些简单逻辑"+city;
   }
}

这时我们就需要写一些测试用例去验证一下了。
场景三:当我们有自定义的方法的时候,就可能需要测试一下,看看返回结果是否满足需求,代码如下所示。

public interface AddressRepository extends JpaRepository<Address, Long>{
    Page<Address> findByAddress(@Param("address") String address, Pageable pageable);
}

场景四:当我们利用 @Query注解,写了一些 JPQL 或者 SQL 的时候,就需要写一次测试用例来验证一下,代码如下所示。

public interface AddressRepository extends JpaRepository<Address, Long>{
    //通过@Query注解自定的JPQL或Navicat SQL
    @Query(value = "FROM Address where deleted=false ")
    Page<Address> findAll(Pageable pageable);
}

那么对应的复杂一点的测试用例就要变成如下面这段代码所示的样子。

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DataJpaTest
public class AddressRepositoryTest {
    @Autowired
    private AddressRepository addressRepository;
    @BeforeAll //利用 @BeforeAll准备一些Repositroy需要的测数据
    @Rollback(false)// 由于每个方法都是有事务回滚机制的,为了测试我们的Repository可能需要模拟一些数据,所以我们改变回滚机制
    @Transactional
    public void init() {
        Address address = Address.builder().city("shanghaiDeleted").deleted(true).build();
        addressRepository.save(address);
    }
    //测试没有包含删除的记录
    @Test
    public  void testFindAllNoDeleted() {
        List<Address> address1 = addressRepository.findAll();
        int deleteSize = address1.stream().filter(d->d.equals("shanghaiDeleted")).collect(Collectors.toList()).size();
        Assertions.assertTrue(deleteSize==0); //测试一下不包含删除的条数
    }
}

场景五:当我们测试一些 JPA 或者 Hibernate 的底层特性的时候,测试用例可以很好地帮助我们。因为如果依赖项目启动来做测试,效率太低了,例如我们之前讲的一些 @PersistenceContext 特性,那么就可以通过类似如下的测试用例完成测试。

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DataJpaTest
@Import(TestConfiguration.class)
public class UserInfoRepositoryTest {
    @Autowired
    private UserInfoRepository userInfoRepository;
    //测试一些手动flush的机制
    @PersistenceContext
            (properties = {@PersistenceProperty(
                    name = "org.hibernate.flushMode",
                    value = "MANUAL"//手动flush
            )})
    private EntityManager entityManager;
<span class="hljs-meta">@BeforeAll</span>
<span class="hljs-meta">@Rollback(false)</span>
<span class="hljs-meta">@Transactional</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-comment">//提前准备一些数据方便我们测试</span>
    UserInfo u1 = UserInfo.builder().id(<span class="hljs-number">1L</span>).lastName(<span class="hljs-string">"jack"</span>).version(<span class="hljs-number">1</span>).build();
    userInfoRepository.save(u1);
}
<span class="hljs-meta">@Test</span>
<span class="hljs-meta">@Transactional</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testLife</span><span class="hljs-params">()</span> </span>{
    UserInfo userInfo = UserInfo.builder().name(<span class="hljs-string">"new name"</span>).build();
    <span class="hljs-comment">//新增一个对象userInfo交给PersistenceContext管理,即一级缓存</span>
    entityManager.persist(userInfo);
    <span class="hljs-comment">//此时没有detach和clear之前,flush的时候还会产生更新SQL</span>
    userInfo.setName(<span class="hljs-string">"old name"</span>);
    entityManager.flush();
    entityManager.clear();

// entityManager.detach(userInfo);
// entityManager已经clear,此时已经不会对UserInfo进行更新了
userInfo.setName(“new name 11”);
entityManager.flush();
//由于有cache机制,相同的对象查询只会触发一次查询SQL
UserInfo u1 = userInfoRepository.findById(1L).get();
//to do some thing
UserInfo u2 = userInfoRepository.findById(1L).get();
}
}

测试场景可能远不止我上面举例的这些,总之你要灵活地利用测试用例来判断某些方法或者配置是否达成预期效果还是挺方便的。其中初始化数据的方法也有很多,我也只是举一个例子,期望你可以举一反三。

以上我们利用 @DataJpaTest 帮我们完成了数据层的集成测试,但是实际工作中,我们也会用到纯粹的单元测试,那么集成测试和单元测区别是什么?我们必须要搞清楚。

集成测试和单元测试的区别

什么是单元测试

通俗来讲,就是不依赖本类之外的任何方法完成本类里面的所有方法的测试,也就是我们常说的依赖本类之外的,都通过 Mock 的方式进行。那么在单元测试的模式下,我们一起看看 Service 层的单元测试应该怎么写。

Service 层单元测试

首先,我们模拟一个业务中的 Service 方法,代码如下所示。

@Component
public class UserInfoServiceImpl implements UserInfoService {
   @Autowired
   private UserInfoRepository userInfoRepository;
   //假设有个 findByUserId的方法经过一些业务逻辑计算返回了一个业务对象UserInfoDto
   @Override
   public UserInfoDto findByUserId(Long userId) {
      UserInfo userInfo = userInfoRepository.findById(userId).orElse(new UserInfo());
      //模拟一些业务计算改变一下name的值返回
      UserInfoDto userInfoDto = UserInfoDto.builder().name(userInfo.getName()+"_HELLO").id(userInfo.getId()).build();
      return userInfoDto;
   }
}

其次,service 通过 Spring 的 @Component 注解进行加载,UserInfoRepository 通过 spring 的 @Autowired 注入进来,我们来测试一下 findByUserId 这个业务 service 方法,单元测试写法如下。

@ExtendWith(SpringExtension.class)//通过这个注解利用Spring的容器
@Import(UserInfoServiceImpl.class)//导入要测试的UserInfoServiceImpl
public class UserInfoServiceTest {
    @Autowired //利用spring的容器,导入要测试的UserInfoService
    private UserInfoService userInfoService;
    @MockBean //里面@MockBean模拟我们service中用到的userInfoRepository,这样避免真实请求数据库
    private UserInfoRepository userInfoRepository;
    // 利用单元测试的思想,mock userInfoService里面的UserInfoRepository,这样Service层就不用连接数据库,就可以测试自己的业务逻辑了
    @Test
    public void testGetUserInfoDto() {
//利用Mockito模拟当调用findById(1)的时候,返回模拟数据
                Mockito.when(userInfoRepository.findById(1L)).thenReturn(java.util.Optional.ofNullable(UserInfo.builder().name("jack").id(1L).build()));
        UserInfoDto userInfoDto = userInfoService.findByUserId(1L);
        //经过一些service里面的逻辑计算,我们验证一下返回结果是否正确
        Assertions.assertEquals("jack",userInfoDto.getName());
    }
}

这样就可以完成了我们的 Service 层的测试了。

其中 @ExtendWith(SpringExtension.class) 是 spring boot 与 Junit 5 结合使用的时候,当利用 Spring 的 TesatContext 进行 mock 测试时要使用的。有的时候如果们做一些简单 Util 的测试,就不一定会用到 SpringExtension.class。

在 service 的单元测试中,主要用到的知识点有四个。

  1. 通过 @ExtendWith(SpringExtension.class) 加载 Spring 的测试框架及其 TestContext;

  2. 通过 @Import(UserInfoServiceImpl.class) 导入具体要测试的类,这样 SpringTestContext 就不用加载项目里面的所有类,只需要加载 UserInfoServiceImpl.class 就可以了,这样可以大大提高测试用例的执行速度;

  3. 通过 @MockBean 模拟 UserInfoSerceImpl 依赖的 userInfoRepository,并且自动注入 Spring test context 里面,这样 Service 里面就自动有依赖了;

  4. 利用 Mockito.when().thenReturn() 的机制,模拟测试方法。

这样我们就可以通过 Assertions 里面的断言来测试 serice 方法里面的逻辑是否符合预期了。那么接下来我们看看 Controller 层的测试用例要怎么写。

Controller 层单元测试

我们新增一个 UserInfoController 跟进 Id 获得 UserInfoDto 的信息,代码如下所示。

@RestController
public class UserInfoController {
   @Autowired
   private UserInfoService userInfoService;
   //跟进UserId取用户的详细信息
   @GetMapping("/user/{userId}")
   public UserInfoDto findByUserId(@PathVariable Long userId) {
      return userInfoService.findByUserId(userId);
   }
}

那么我们看一下 Controller 里面完整的测试用例,代码如下所示。

package com.example.jpa.demo;
import com.example.jpa.demo.service.UserInfoService;
import com.example.jpa.demo.service.dto.UserInfoDto;
import com.example.jpa.demo.web.UserInfoController;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserInfoController.class)
public class UserInfoControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private UserInfoService userInfoService;
<span class="hljs-comment">//单元测试mvc的controller的方法</span>
<span class="hljs-meta">@Test</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testGetUserDto</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
    <span class="hljs-comment">//利用@MockBean,当调用 userInfoService的findByUserId(1)的时候返回一个模拟的UserInfoDto数据</span>
    Mockito.when(userInfoService.findByUserId(<span class="hljs-number">1L</span>)).thenReturn(UserInfoDto.builder().name(<span class="hljs-string">"jack"</span>).id(<span class="hljs-number">1L</span>).build());
    
    <span class="hljs-comment">//利用mvc验证一下Controller里面的解决是否OK</span>
    MockHttpServletResponse response = mvc
            .perform(MockMvcRequestBuilders
                    .get(<span class="hljs-string">"/user/1/"</span>)<span class="hljs-comment">//请求的path</span>
                    .accept(MediaType.APPLICATION_JSON)<span class="hljs-comment">//请求的mediaType,这里面可以加上各种我们需要的Header</span>
            )
            .andDo(print())<span class="hljs-comment">//打印一下</span>
            .andExpect(status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath(<span class="hljs-string">"$.name"</span>).value(<span class="hljs-string">"jack"</span>))
            .andReturn().getResponse();
    System.out.println(response);
}

}

其中我们主要利用了 @WebMvcTest 注解,来引入我们要测试的 Controller。打开 @WebMvcTest 可以看到关键源码,如下图所示。

Drawing 1.png

我们可以看得出来,@WebMvcTest 帮我们加载了 @ExtendWith(SpringExtension.class),所以不需要额外指定,就拥有了 Spring 的 test context,并且也自动加载了 mvc 所需要的上下文 WebMvctestContextbootstrapper。

有的时候可能有一些全局的 Filter,也可以通过此注解里面的 includeFilters 和 excluedeFilters 加载和排除我们需要的 WebMvcFilter 进行测试。

当通过 @WebMvcTest(UserInfoController.class) 导入我们需要测试的 Controller 之后,就可以再通过 MockMvc 请求到我们加载的 Contoller 里面的 path 了,并且可以通过 MockMvc 提供的一些方法发送请求,验证 Controller 的响应结果。

下面概括一下 Contoller 层单元测试主要用到的三个知识点。

  1. 利用 @WebMvcTest 注解,加载我们要测试的 Controller,同时生成 mvc 所需要的 Test Context;

  2. 利用 @MockBean 默认 Controller 里面的依赖,如 Service,并通过 Mockito.when().thenReturn();的语法 mock 依赖的测试数据;

  3. 利用 MockMvc 中提供的方法,发送 Controller 的 Rest 风格的请求,并验证返回结果和状态码。

那么单元测试我们先介绍这么多,下面看一下什么是集成测试。

什么是集成测试

顾名思义,就是指多个模块放在一起测试,和单元测试正好相反,并非采用 mock 的方式测试,而是通过直接调用的方式进行测试。也就是说我们依赖 spring 容器进行开发,所有的类之间直接调用,模拟应用真实启动时候的状态。我们先从 Service 层进行了解。

Service 层的基层测试用例写法

我们还用刚才的例子,看一下 UserInfoService 里面的 findByUserId 通过集成测试如何进行。测试用例的写法如下。

@DataJpaTest
@ComponentScan(basePackageClasses= UserInfoServiceImpl.class)
public class UserInfoServiceIntegrationTest {
    @Autowired
    private UserInfoService userInfoService;
    @Autowired
    private UserInfoRepository userInfoRepository;
    @Test
    @Rollback(false)//如果我们事务回滚设置成false的话,数据库可以真实看到这条数据
    public void testIntegtation() {
        UserInfo u1 = UserInfo.builder().name("jack-db").ages(20).id(1L).telephone("1233456").build();
        //数据库真实加一条数据
        userInfoRepository.save(u1);//数据库里面真实保存一条数据
        UserInfoDto userInfoDto =  userInfoService.findByUserId(1L);
        userInfoDto.getName();
        Assertions.assertEquals(userInfoDto.getName(),u1.getName()+"_HELLO");
    }
}

我们执行一下测试用例,结果如下图所示。

Drawing 2.png

这时你会发现数据已经不再回滚,也会正常地执行 SQL,而不是通过 Mock 的方式测试。Service 的集成测试相对来说还比较简单,那么我们看下 Controller 层的集成测试用例应该怎么写。

Controller 层的集成测试用例的写法

我们用集成测试把刚才 UserInfoCotroller 写的 user/1/ 接口测试一下,将集成测试的代码做如下改动。

@SpringBootTest(classes = DemoApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //加载DemoApplication,指定一个随机端口
public class UserInfoControllerIntegrationTest {
    @LocalServerPort //获得模拟的随机端口
    private int port;
    @Autowired //我们利用RestTemplate,发送一个请求
    private TestRestTemplate restTemplate;
    @Test
    public void testAllUserDtoIntegration() {
        UserInfoDto userInfoDto = this.restTemplate
                .getForObject("http://localhost:" + port + "/user/1", UserInfoDto.class);//真实请求有一个后台的API
        Assertions.assertNotNull(userInfoDto);
    }
}

我们再看日志的话,会发现此次的测试用例会在内部启动一个 tomcat 容器,然后再利用 TestResTemplate 进行真实请求,返回测试结果进行测试。

而其中会涉及一个注解 @SpringBootTest,它用来指定 Spring 应用的类是哪个,也就是我们真实项目的 Application 启动类;然后会指定一个端口,此处必须使用随机端口,否则可能会有冲突(如果我们启动的集成测试有点多的情况)。日志如下图所示。

Drawing 3.png

如果我们看 @SprintBootTest 源码的话,会发现这个注解也是加载了 Spring 的测试环境 SpringExtension.class,并且里面有很多属性可以设置,测试的时候的配置文件 properties 和一些启动的环境变量 WebEnv;然后我们又利用了 Spring Boot Test 提供的 @LocalServerPort 获得启动时候的端口。源码如下图所示。

Drawing 4.png

虽然集成测试用法也是比较简单的,甚至可能比 Mock 的测试环境更简单,因为集成测试可以取到 Application 启动之后加载的任何 Bean。但是实际工作中我们使用集成测试的时候,还是需要思考一些问题。

集成测试的一些思考

1.所有的方法都需要集成测试吗?

这是我们写集成测试用的时候需要思考的,因为集成测试用例需要内部启动 Tomcat 容器,所以可能会启动得慢一点。如果我们的项目加载的配置文件越来越多,势必会导致测试也会变慢。假设我们测试一个简单的逻辑就需要启动整个 Application,那么显然是不妥的。

那么我们整个 Application 不需要集成测试吗?也显然不是的,因为有些时候只有集成在一起才会发生问题,最简单的一个集成测试是我们需要测试是否能正常启动,所以一个项目里面会有个 ApplicationTests 来测试项目是否能正常启动。代码如下所示。

@SpringBootTest
class DemoApplicationTests {
//测试项目是否能正常启动
   @Test
   void contextLoads() {
   }
}

2.一定是非集成测试就是单元测试吗?

实际工作中并没有划分那么清楚,有的时候我们集成了 N 个组件一起测试,可能就是不连数据库。比如我们可能会使用 Feign-Client 根据第三方的接口获取一些数据,那么我们正常的做法就是新建一个 Service,代码如下所示。

/**
 * 测试普通JSON返回结果,根据第三方接口取一个数据
 */
@FeignClient(name = "aocFeignTest", url = "http://room-api.staging.jack.net")
interface AppSettingService {
    @GetMapping("/api/v1/app/globalSettings")
    HashMap<String,Object> getAppSettings();
}

那么这个时候如果我们要测试,显然不需要启动整个 Application 来完成,但是需要按需加载一些 Configration 才能测试,那么测试用例会变成如下情况。

@ExtendWith(SpringExtension.class)//利用Spring上下文
@Import({FeignSimpleConfiguration.class, FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class})//导入此处Fegin-Client测试所需要的配置文件
@EnableFeignClients(clients = AppSettingService.class)//通过FeignClient的注解加载AppSettingService客户端。
/**
 * 依赖HTTPMessageConverter的使用方法(import FeignSimpleConfiguration junit)
 */
public class FeignJsonTest {
    @Autowired利用Spring的上下文注入appSettingService
    private AppSettingService appSettingService;
    @Test
    public void testJsonFeignClient() {
        HashMap<String,Object>  r = .getAppSettings();
        Assert.assertNotNull(r.get("data"));//测试一下接口返回的结果
    }
}

你会发现这个时候其实并没有启动这个 Application,但是我们也集成了 Fegin-Client 所需要的上下文 Configuration,从而利用 SpringExtension 加载所需要依赖的类,完成一次测试。

所以你一定要理解单元测试和集成测试的本质,根据自己的实际需要选择性地加载一些类来完成测试用例,而不是每次测试的时候都需要把所有类都加载一遍,这样返回会使测试用例的时间变长,从而降低工作效率。

由于目前现状是 Junit 4 和 Junit 5 一样流行,所以你使用的时候要注意使用的是 Junit 5 还是 Junit 4,不要弄混了。下面我再介绍一些 Junit 5 和 Junit 4 的区别,来帮你加深印象。

Junit 4 和 Junit 5 在 Spring Boot 中的区别

第一,Spring Boot 2.2+ 以上的版本默认导入的是 Junit 5 的 jar 包依赖,以下的版本默认导入的是 Junit 4 的 jar 包依赖的版本,所以你在使用不同版本的 Spring Boot 的时候需要注意一下依赖的 jar 包是否齐全。

第二,org.junit.junit.Test 变成了 org.junit.jupiter.api.Test。

第三,一些注解发生了变化:

  • @Before 变成了 @BeforeEach

  • @After 变成了 @AfterEach

  • @BeforeClass 变成了 @BeforeAll

  • @AfterClass 变成了 @AfterAll

  • @Ignore 变成了 @Disabled

  • @Category 变成了 @Tag

  • @Rule 和 @ClassRule 没有了,用 @ExtendWith 和 @RegisterExtension 代替

第四,引用 Spring 的上下文 @RunWith(SpringRunner.class) 变成了 @ExtendWith(SpringExtension.class)。

第五,org.junit.Assert 下面的断言都移到了org.junit.jupiter.api.Assertions 下面,所以一些断言的写法会发生如下变化:

//junit4断言的写法
Assert.assertEquals(200, result.getStatusCodeValue());
Assert.assertEquals(true, result.getBody().contains("employeeList"));
//junit5断言的写法
Assertions.assertEquals(400, ex.getRawStatusCode());
Assertions.assertEquals(true, ex.getResponseBodyAsString().contains("Missing request header"));

第六,Junit 5 提供 @DisplayName("Test MyClass") 用来标识此次单元测试的名字,代码如下所示。

@DisplayName("Test MyClass")
class MyClassTest {
    @Test
    @DisplayName("Verify MyClass.myMethod returns true")
    void testMyMethod() throws Exception {    
        // ...
    }
}

常用的变化我就介绍这么多了,很多时候我看身边的开发人员写测试用例的时候,没有自己的知识脉络,从网上看到一块代码就 Copy 过来了,那么希望你可以通过上面的介绍,认清楚测试用例的整体情况,在实际开发过程中再根据具体的语法进行调整。

总结

这一讲我们主要介绍了 @DataJpaTest、@ExtendWith(SpringExtension.class)、@MockBean、@WebMvcTest、@MockMvc、@SpringBootTest 等注解的用法,明白了测试用例中几个最关键的集成测试和单元测试的区别,知道了测试用例中如何利用 Spring 的上下文,也晓得了 Junit 5 的一些基本语法。

这里我只是带领你入了测试用例的门,剩下的期望你能结合自己的实际情况,将单元测试重视起来,并灵活运用 Spring Boot 给我们带来的单元测试的便利性,完成必要的单元测试,从而减少代码的 bug 率,提升自己的开发效率。

本讲内容到这里就结束了,下一讲我们来聊聊 Spring Data 和 ES 如何结合使用。再见。


32 Spring Data ElasticSearch 在 Spring Data 中的用法有哪些?

这一讲是这门专栏的最后一讲了,恭喜你一直坚持到现在。

相信到这里,你已经对 Spring Data JPA 有一定的认识了,那么这一讲我会为你演示 Spring Data ElasticSearch 如何使用,帮助你打开思路,感受 Spring Data 的抽象封装。

我们还是从一个案例入手。

Spring Data ElasticSearch 入门案例

Spring Data 和 Elasticsearch 结合的时候,唯一需要注意的是版本之间的兼容性问题,Elasticsearch 和 Spring Boot 是同时向前发展的,而 Elasticsearch 的大版本之间还存在一定的 API 兼容性问题,所以我们必须要知道这些版本之间的关系,我整理了一个表格,如下。

Spring Data Release TrainSpring Data ElasticsearchElasticsearchSpring Boot
2020.0.0[1]4.1.x[1]7.9.32.4.x[1]
Neumann4.0.x7.6.22.3.x
Moore3.2.x6.8.122.2.x
Lovelace3.1.x6.2.22.1.x
Kay[2]3.0.x[2]5.5.02.0.x[2]
Ingalls[2]2.1.x[2]2.4.01.5.x[2]

现在你对这些版本之间的关联关系有了一定印象,由于版本越新越便利,所以一般情况下我们直接采用最新的版本。

接下来看看这个版本是怎么完成 Demo 演示的。

第一步:利用 Helm Chart 安装一个 Elasticsearch 集群 7.9.3 版本,执行命令如下。

1. helm2 repo add elastic https://helm.elastic.co
2. helm2 install --name myelasticsearch elastic/elasticsearch  --set imageTag=7.9.3

安装完之后,我们就可以看到如下信息。

Drawing 0.png

这代表我们安装成功。

由于 ElasticSearch 是发展变化的,所以它的安装方式你可以参考官方文档:https://github.com/elastic/helm-charts/tree/master/elasticsearch

然后我们利用 k8s 集群端口映射到本地,就可以开始测试了。

~ ❯❯❯ kubectl port-forward svc/elasticsearch-master 9200:9200 -n my-namespace
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200

第二步:在 gradle.build 里面配置 Spring Data ElasticSearch 依赖的 Jar 包

我们依赖 Spring Boot 2.4.1 版本,完整的 gradle.build 文件如下所示。

plugins {
   id 'org.springframework.boot' version '2.4.1'
   id 'io.spring.dependency-management' version '1.0.10.RELEASE'
   id 'java'
}
group = 'com.example.data.es'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
   compileOnly {
      extendsFrom annotationProcessor
   }
}
repositories {
   mavenCentral()
}
dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-actuator'
   implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
   implementation 'org.springframework.boot:spring-boot-starter-web'
   compileOnly 'org.projectlombok:lombok'
   developmentOnly 'org.springframework.boot:spring-boot-devtools'
   runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
   annotationProcessor 'org.projectlombok:lombok'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
   useJUnitPlatform()
}

第三步:新建一个目录,结构如下图所示,方便我们测试

Drawing 1.png

第四步:在 application.properties 里面新增 es 的连接地址,连接本地的 Elasticsearch

spring.data.elasticsearch.client.reactive.endpoints=127.0.0.1:9200

第五步:新增一个 ElasticSearchConfiguration 的配置文件,主要是为了开启扫描的包

package com.example.data.es.demo.es;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
//利用@EnableElasticsearchRepositories注解指定Elasticsearch相关的Repository的包路径在哪里
@EnableElasticsearchRepositories(basePackages = "com.example.data.es.demo.es")
@Configuration
public class ElasticSearchConfiguration {
}

第六步:我们新增一个 Topic 的 Document,它类似 JPA 里面的实体,用来保存和读取 Topic 的数据,代码如下所示。

package com.example.data.es.demo.es;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.List;
@Data
@Builder
@Document(indexName = "topic")
@ToString(callSuper = true)
//论坛主题信息
public class Topic {
    @Id
    private Long id;
    private String title;
    @Field(type = FieldType.Nested, includeInParent = true)
    private List<Author> authors;
}
package com.example.data.es.demo.es;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
//作者信息
public class Author {
    private String name;
}

第七步:新建一个 Elasticsearch 的 Repository,用来对 Elasticsearch 索引的增删改查,代码如下所示。

package com.example.data.es.demo.es;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
//类似JPA一样直接操作Topic类型的索引
public interface TopicRepository extends ElasticsearchRepository<Topic,Long> {
    List<Topic> findByTitle(String title);
}

第八步: 新建一个 Controller,对 Topic 索引进行查询和添加。

@RestController
public class TopicController {
    @Autowired
    private TopicRepository topicRepository;
    //查询topic的所有索引
    @GetMapping("topics")
    public List<Topic> query(@Param("title") String title) {
        return topicRepository.findByTitle(title);
    }
    //保存 topic索引
    @PostMapping("topics")
    public Topic create(@RequestBody Topic topic) {
        return topicRepository.save(topic);
    }
}

第九步:发送一个添加和查询的请求测试一下

我们发送三个 POST 请求,添加三条索引,代码如下所示。

POST /topics HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: d9cc1f6c-24dd-17ff-f2e8-3063fa6b86fc
{
    "title":"jack",
    "id":2,
    "authors":[{
        "name":"jk1"
        },{
        "name":"jk2"
        }]
}

然后发送一个 get 请求,获得标题是 jack 的索引,如下面这行代码所示。

GET http://127.0.0.1:8080/topics?title=jack

得到如下结果。

GET http://127.0.0.1:8080/topics?title=jack
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 30 Dec 2020 15:12:16 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
  {
    "id": 1,
    "title": "jack",
    "authors": [
      {
        "name": "jk1"
      },
      {
        "name": "jk2"
      }
    ]
  },
  {
    "id": 3,
    "title": "jack",
    "authors": [
      {
        "name": "jk1"
      },
      {
        "name": "jk2"
      }
    ]
  },
  {
    "id": 2,
    "title": "jack",
    "authors": [
      {
        "name": "jk1"
      },
      {
        "name": "jk2"
      }
    ]
  }
]
Response code: 200; Time: 348ms; Content length: 199 bytes
Cannot preserve cookies, cookie storage file is included in ignored list:
> /Users/jack/Company/git_hub/spring-data-jpa-guide/2.3/elasticsearch-data/.idea/httpRequests/http-client.cookies

这时,一个完整的 Spring Data Elasticsearch 的例子就演示完了。其实你会发现,我们使用 Spring Data Elasticsearch 来操作 ES 相关的 API 的话,比我们直接写 Http 的 client 要简单很多,因为这里面帮我们封装了很多基础逻辑,省去了很多重复造轮子的过程。

其实测试用例也是很简单的,我们接着来看一下写法。

第十步:Elasticsearch Repository 的测试用例写法,如下面的代码和注释所示。

package com.example.data.es.demo;
import com.example.data.es.demo.es.Author;
import com.example.data.es.demo.es.Topic;
import com.example.data.es.demo.es.TopicRepository;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import java.util.List;
@SpringBootTest
@TestPropertySource(properties = {"logging.level.org.springframework.data.elasticsearch.core=TRACE", "logging.level.org.springframework.data.elasticsearch.client=trace", "logging.level.org.elasticsearch.client=TRACE", "logging.level.org.apache.http=TRACE"})//新增一些配置, 开启spring data elastic search的http的调用过程,我们可以查看一下日志
public class ElasticSearchRepositoryTest {
    @Autowired
    private TopicRepository topicRepository;
    @BeforeEach
    public void init() {
//        topicRepository.deleteAll(); //可以直接删除所有索引
        Topic topic = Topic.builder().id(11L).title("jacktest").authors(Lists.newArrayList(Author.builder().name("jk1").build())).build();
        topicRepository.save(topic);//集成测试保存索引
        Topic topic1 = Topic.builder().id(14L).title("jacktest").authors(Lists.newArrayList(Author.builder().name("jk1").build())).build();
        topicRepository.save(topic1);
        Topic topic2 = Topic.builder().id(15L).title("jacktest").authors(Lists.newArrayList(Author.builder().name("jk1").build())).build();
        topicRepository.save(topic2);//保存索引
    }
    @Test
    public void testTopic() {
        Iterable<Topic> topics = topicRepository.findAll();
        topics.forEach(topic1 -> {
            System.out.println(topic1);
        });
        List<Topic> topicList = topicRepository.findByTitle("jacktest");
        topicList.forEach(t -> {
            System.out.println(t);//获得索引的查询结果
        });
        List<Topic> topicList2 = topicRepository.findByTitle("xxx");
        topicList2.forEach(t -> {
            System.out.println(t);//我们也可以用上一讲介绍的断言测试
        });
    }
}

接着我们看一下测试用例的调用日志,从日志可以看出,调用的时候发生的 Http 的 PUT 请求,是用来创建和修改一个索引的文档的。请看下面的图片。

Drawing 2.png

从中也可以看得出来,转化成 es 的 api 查询语法之后,发送的 post 请求又变成下图显示的样子。

Drawing 3.png

日志比较长,你有兴趣的话,可以按照我的 DEMO 和开启日志的方法,自己去分析体会一下。

下面来说说 Spring Data ElasticSearch 中关键的几个类。

Spring Data ElasticSearch 关键的类

通过上面的案例我们可以知道,Spring Data ElasticSearch 的用法其实非常简单,并且我们通过日志也可以看到,底层实现是基于 http 请求,来操作 Elasticsearch 的 server 中的 api 进行的。

那么我们简单看一下这一框架还给我们提供了哪些 ElasticSearch 的操作方法。和分析 Spring Data JPA 一样,看一下 Repository 的所有子类,如下图所示。

Drawing 4.png

从图中可以看得出来,ElasticsearchRepository 是默认的 Repository 的实现类,我们如果继续往下面看源码的话,就可以看到里面进行了很多 ES 的 Http Client 操作。

同时再看一下 Structure 视图,如下所示。

Drawing 5.png

从这张图可以知道,ElasticsearchRepository 默认给我们提供了 search 和 index 相关的一些操作方法,并且 Spring Data Common 里面的一些公共方法同样适用,这和我们刚才演示的 Defining Method Query 的 JPA 语法同样适用,可以大大减轻操作 ES 的难度,提高了开发的效率,甚至像我们没有演示到的分页、排序、limit 等同样适用。

所以你现在学到了一个“套路”:和 Spring Data JPA 用相同的思路,就可以很快掌握 Spring Data Elasticsearch 的基本用法,及其大概的实现思路。

那么很多时候同一个工程里面既有 JPA 又有 Elasticsearch,又该怎么写呢?

ESRepository 和 JPARepository 同时存在

这个时候应该怎么区分不同的 Repository 用什么呢?

我们假设刚才测试的样例里面,同时有关于 User 信息的 DB 操作,那么看一下我们的项目应该怎么写。

第一步:我们将对 Elasticsearch 的实体、Repository 和对 JPA 操作的实体、Repository 放到不同的文件里面,如下图所示。

Drawing 6.png

第二步:新增 JpaConfiguration,用来指定 Jpa 相关的 Repository 目录,完整代码如下。

package com.example.data.es.demo.jpa;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
//利用@EnableJpaRepositories指定JPA的目录是"com.example.data.es.demo.jpa"
@EnableJpaRepositories(basePackages = "com.example.data.es.demo.jpa")
@Configuration
public class JpaConfiguration {
}

第三步:新增 User 实体,用来操作用户基本信息

@Data
@Builder
@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
}

第四步:新增 UserRepository,用来进行 DB 操作

package com.example.data.es.demo.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
//对User的DB操作,我们直接继承JpaRepository
public interface UserRepository extends JpaRepository<User,Long> {
}

第五步:写测试用例进行测试

package com.example.data.es.demo;
import com.example.data.es.demo.jpa.User;
import com.example.data.es.demo.jpa.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
//利用@DataJpaTest完成集成测试
@DataJpaTest
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testJpa() {
//往数据库里面保存一条数据,并且打印一下
                userRepository.save(User.builder().id(1L).name("jkdb").email("jack@email.com").build());
        List<User> users = userRepository.findAll();
        users.forEach(user -> {
            System.out.println(user);
        });
    }
}

这个时候,我们的测试用例就变成了如下图所示的结构。

Drawing 7.png

那么现在我们知道了,JPA 和 Elasticsearch 同时存在,和启动项目是一样的效果,这里就不写 Controller 了。

我们再整体运行一下这三个测试用例,进行完整的测试,就可以看到如下结果。

1.ElasticSearchRepositoryTest 执行的时候,通过日志可以看到这是对 ES 进行的操作,如下图所示。

Drawing 8.png

2.UserRepositoryTest 执行的时候,通过日志我们可以看出来这是对 DB 进行的操作,所以谁也不影响谁,如下图所示。

Drawing 9.png

通过上面的例子我们可以知道,Spring Data 对 JPA 等 SQL 型的传统数据库的支持是非常好的,同时对 NoSQL 型的非关系类数据库的支持也比较友好,大大降低了操作不同数据源的难度,可以有效提升我们的开发效率。

总结

这一讲内容到这里就结束了,我通过“入门型”的 Spring Data Elasticsearch 样例展示,让你体会了 Spring Data 对数据操作的抽象封装的强大之处。

如果你研究好了这部分内容,其实 Spring Data 中的其他系列也是可以通用的。这里我只是期望起到抛砖引玉的效果,希望你能更好地掌握 Spring Data 的精髓,并且能深入理解 JPA。

至此,我们的专栏也将告一段落,不知道这 32 讲的内容对你是否有帮助,我希望你可以回过头好好回顾,更好地掌握 Spring Data JPA。

如果对此有不懂的地方,也欢迎你在评论区留言,我们一起探讨,一起在这条路上不断精进。再见!

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa


结束语 师傅领进门,修行靠个人

你好,专栏的学习到这里已经进入尾声了,不知道这 32 讲的内容,是否对你有帮助。那么这一讲我们来回顾一下整个课程的内容。

课程回顾

本专栏一共分为四个部分:基础知识、高级用法与实践、原理与问题排查以及思路扩展。这四个部分的内容,完整地为你呈现了 Spring Data JPA 的基本用法,以及工作中的一些最佳实践,并且几乎在每一讲中,我都带你利用源码剖析了核心原理。

基本用法方面

希望你能够熟练掌握如下内容:

  1. JPA 的 Repository 的用法;

  2. Defining Query Method 的用法,以及不同的参数和返回结果是如何实现的;

  3. 熟练使用 @Query 查询,以及 @Entity、@EntityGraph、Jackson 等方面的注解;

  4. 熟知 JPA 对 WebMVC 做的支持有审计、分页、排序和参数扩展;

  5. 学会自定义 Repository;

  6. 掌握多数据源的配置方法。

原理方面

希望你能掌握如下内容,并能熟练解决实际工作中的高级问题。

  1. 数据源、连接池、、Connection、事务、Session 分别是什么,以及它们之间的关系是怎么样的;

  2. 掌握 Persistence Context、数据的 flush 时机、flush 对事务的影响;

  3. 了解 Open-in-view 是如何对 Lazy Exception 产生影响的,Lazy 的原理又是什么;

  4. 知道 N+1 SQL 指的是什么、如何产生的,解决的方法有哪些。

其实以上列举这些问题都可以从课程中获取答案。同时你还可以通过我为你提供的思路扩展篇,感受一下 JPA 的实体预定给未来的协议带来的影响,那时你就知道了 Spring Data 的强大之处。

因此可以说,学好 Spring Data 是非常重要的,从中我们可以学习到“Spring 大神们”强悍、抽象的封装能力,并且可以大大地提高日常开发效率。

修行靠个人

在课程中,我所总结的经验是有限的,可能无法考虑到全部场景,所以这门专栏我只是把个人所见尽可能地分享给你作为参考。

其实 Spring 的版本是不断变化的,源码也不断在改进。所以希望通过学习,你可以掌握文中我所介绍的学习方法以及阅读源码的技巧,并不断修炼、思考,找到属于自己的学习方法论。

如果你能完整地把课程看完,接下来一定要付诸行动。在工作中,不断总结遇到的疑难杂症,想想原理,同时善于思考并且积极解决,这样自己才能真正地成长。

我也欢迎你能把遇到的一些难题提交到 Github 里面对应的 issue。

关于个人修行方面,我在此给你几点建议:

  1. 掌握学习思路,总结出来自己的一套方法论;

  2. 学会看源码,学会 debug,看完专栏课程或者是官方的文档后,要学会通过源码对应理解,这样才能验证别人说的是否合理,因为文章可能会过时,但是源码一定不会骗人;

  3. 学会举一反三,掌握解决问题的思路,凡事想一下最佳实践是什么,是否有什么更优解;

  4. 如果你写出来的代码或者逻辑别人不好理解,那么肯定有改进空间。

最后,当我们万事俱备的时候,就期待一下未来的变化吧。

未来可期

我个人感觉,Spring Data JPA 的抽象和封装还是非常灵活的,它绝对被行业所低估了。

如果是 Spring 项目,你会发现身边很多新的项目会逐渐采用 Spring Data 这套体系,那么当你熟悉和掌握得非常精通之后,你会发现你再也不想用 Mybatis 了,因为工作太烦琐了,这就好比刚开始你不习惯在用 Mac 笔记本的时候不用鼠标,但是一旦你习惯了 Mac 的触摸板,就再也不想用鼠标了。同理,当你掌握了高效率的工作方式、编程方式之后,你就会发现那些低效率的开发实在是太费事了。

总之呢,学习就是一场永无止境的修行。我希望你能通过学习有个好的发展,成为某些领域的大神。同时保持好心态,积极与同行交流,这样才能共同成长。也欢迎你分享自己的 JPA 使用经验,我们一同探讨。

我是张振华,很高兴遇见你,后会有期。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值