Menu Close

如何在Spring Boot项目中使用Spring Data JPA

1. 创建Spring Boot项目

我们将使用Spring Initializr 工具引导我们的应用程序。

请访问http://start.spring.io。

项目名称为:spring-boot-crud-example

在Language部分中选择Java。

在 Artifact [ˈɑrtɪˌfækt] 中填入:spring-boot-crud-example

file

添加Web、Lombok、JPA和MariaDB依赖项。

单击生成以生成并下载项目。

file

2. 配置 Gradle

通过 文件 -> 设置 -> 构建、执行、部署 -> 构建工具 -> Gradle 来设置 Gradle 的一些属性。

file

同时修改 ./gradle/wrapper/gradle-wrapper.properties 文件,gradle 使用本地下载的 7.5版本,同时使用代理IP(不需要代理可删除相关配置)

gradle-wrapper.properties文件内容

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=file:///data/gradle-7.5/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
systemProp.http.proxyHost=XXX.XXX.XXX.XXX
systemProp.http.proxyPort=3128
systemProp.https.proxyHost=XXX.XXX.XXX.XXX
systemProp.https.proxyPort=3128

3. Maven依赖

编辑 ./build.gradle.kts 文件,将 maven 中央仓库源修改为阿里仓库源。

build.gradle.kts文件样例

plugins {
    java
    id("org.springframework.boot") version "2.7.6"
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
}

group = "xin.qishuo"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    maven ("https://maven.aliyun.com/repository/jcenter")
    maven ( "https://maven.aliyun.com/repository/google")
    maven ( "https://maven.aliyun.com/repository/central")
    maven ( "https://maven.aliyun.com/repository/gradle-plugin")
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    compileOnly("org.projectlombok:lombok")
    runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
    annotationProcessor("org.projectlombok:lombok")
    implementation("com.alibaba.fastjson2:fastjson2:2.0.20")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType <Test> {
    useJUnitPlatform()
}

额外增补了 fastjson2 的依赖包。

4. 配置 MariaDB 数据库

首先在MariaDB服务器中创建一个名为study的数据库。

然后配置数据库URL、用户名和密码,以便Spring Boot可以创建数据源。

打开./src/main/resources/application.properties文件,并向其中添加以下属性:

application.properties文件样例

spring.datasource.url = jdbc:mariadb://172.16.4.43:6006/study?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
spring.datasource.driver-class-name = org.mariadb.jdbc.Driver
spring.datasource.username = study
spring.datasource.password = studyxy00yz
# timeout 60 sec
spring.datasource.hikari.connection-timeout=60000
# max 5
spring.datasource.hikari.maximum-pool-size=5
# spring.jpa.hibernate.ddl-auto = create-drop
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

在该配置中,spring.jpa.hibernate.ddl-auto属性的属性值及其说明如下:

属性值 说明
none 不指定数据库初始化模式
create 当Spring Boot应用运行时,会删除并重新创建数据库。所以每次启动时,所有的数据都会被清空
create-drop 当sessionFactory关闭,表会自动删除
validate 在Spring Boot应用运行时,会检查数据库中的表与java实体类是否匹配。如果不匹配,则运行失败
update 当在java实体类中新增了一个字段,在应用重新运行时,会将字段添加到数据库表中的新列,但不会移除先前存在的列或约束

注意:当数据库是嵌入式数据库时,Spring Boot会指定该属性默认值为create-drop;当不是嵌入式数据库时,Spring Boot指定该属性的默认值为none。

在开发阶段中,通常使用update,但需要注意,update不会移除先前已经存在的列和约束,即使是不再需要的。当产品发布的时候,建议使用none或直接不指定该属性(数据库单独维护)。

至此,我们可以启动该Spring Boot项目:

file

5. 创建 模型(Model) 层

创建一个entity的包名,并在其下创建一个Product类,其中包含以下内容:

实体类Product的样板代码

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "PRODUCT_TBL")
public class Product {

    @Id
    @GeneratedValue
    private int id;

    private String name;

    private int quantity;

    private BigDecimal price;
}

6. 创建 存储库(Repository) 层

创建一个repository的包名,并在其下创建ProductRepository接口,使其继承JpaRepository。具体内容如下:

ProductRepository接口的样板代码

import org.springframework.data.jpa.repository.JpaRepository;
import xin.qishuo.sprintbootcrudexample.entity.Product;

public interface ProductRepository extends JpaRepository <Product,Integer> {
    Product findByName(String name);
}

7. 创建 服务(Service) 层

创建一个service的包名,并在其下创建一个ProductService类,其中包含以下内容:

ProductService类的样板代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.repository.ProductRepository;

import java.util.List;

@Service
public class ProductService {
    @Autowired
    private ProductRepository repository;

    public Product saveProduct(Product product) {
        return repository.save(product);
    }

    public List<Product> saveProducts(List<Product> products) {
        return repository.saveAll(products);
    }

    public List<Product> getProducts() {
        return repository.findAll();
    }

    public Product getProductById(int id) {
        return repository.findById(id).orElse(null);
    }

    public Product getProductByName(String name) {
        return repository.findByName(name);
    }

    public String deleteProduct(int id) {
        repository.deleteById(id);
        return "product removed !! " + id;
    }

    public Product updateProduct(Product product) {
        Product existingProduct = repository.findById(product.getId()).orElse(null);
        existingProduct.setName(product.getName());
        existingProduct.setQuantity(product.getQuantity());
        existingProduct.setPrice(product.getPrice());
        return repository.save(existingProduct);
    }
}

8. 创建 控制器(Controller) 层

创建一个controller的包名,并在其下创建一个ProductController类,其中包含以下内容:

ProductController类的样板代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.service.ProductService;

import java.util.List;

@RestController
public class ProductController {

    @Autowired
    private ProductService service;

    @PostMapping("/addProduct")
    public Product addProduct(@RequestBody Product product) {
        return service.saveProduct(product);
    }

    @PostMapping("/addProducts")
    public List<Product> addProducts(@RequestBody List<Product> products) {
        return service.saveProducts(products);
    }

    @GetMapping("/products")
    public List<Product> findAllProducts() {
        return service.getProducts();
    }

    @GetMapping("/productById/{id}")
    public Product findProductById(@PathVariable int id) {
        return service.getProductById(id);
    }

    @GetMapping("/product/{name}")
    public Product findProductByName(@PathVariable String name) {
        return service.getProductByName(name);
    }

    @PutMapping("/update")
    public Product updateProduct(@RequestBody Product product) {
        return service.updateProduct(product);
    }

    @DeleteMapping("/delete/{id}")
    public String deleteProduct(@PathVariable int id) {
        return service.deleteProduct(id);
    }

9. 运行Spring Boot应用程序

file

10. 使用 SpringBootTest 测试

一旦依赖了spring-boot-starter-test,下面这些类库将被一同依赖进去:

  • JUnit:java测试事实上的标准,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同)。
  • Spring Test & Spring Boot Test:Spring的测试支持。
  • AssertJ:提供了流式的断言方式。
  • Hamcrest:提供了丰富的matcher。
  • Mockito:mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言。
    关于Mock的介绍请参阅:附录
  • JSONassert:为JSON提供了断言功能。
  • JsonPath:为JSON提供了XPATH功能。

编写测试类:TestProductController

TestProductController类的样板代码


import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.repository.ProductRepository;

import java.math.BigDecimal;
import java.net.URI;

@SpringBootTest
@AutoConfigureMockMvc
public class TestProductController {

    @Autowired
    ProductRepository productRepository;

    @Autowired
    private MockMvc mockMvc;

    /**
     * post请求,json数据提交
     *
     * @return
     * @throws Exception
     */
    @Test
    void addProduct() throws Exception {
        Product product = new Product();
        product.setId(2);
        product.setName("earphone");
        product.setPrice(new BigDecimal(29.9));
        product.setQuantity(222);
        String productJsonStr = JSON.toJSONString(product);
        ResultActions ra = mockMvc.perform(MockMvcRequestBuilders
                        .post(new URI("/addProduct"))
                        .content(productJsonStr)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                ).andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("earphone"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.quantity").value(222))
                .andExpect(MockMvcResultMatchers.jsonPath("$.price").value(29.9))
                .andDo(MockMvcResultHandlers.print());
        MvcResult result = ra.andReturn();
        System.out.println("--------return: " + result.getResponse().getContentAsString());
    }

    /**
     * get请求, @PathVariable 路径参数提交
     *
     * @return
     * @throws Exception
     */
    @Test
    void findProductById() throws Exception {
        int id = 1;
        ResultActions ra = mockMvc.perform(MockMvcRequestBuilders
                        .get("/productById/{id}", id)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("book"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.quantity").value(111))
                .andExpect(MockMvcResultMatchers.jsonPath("$.price").value(19.9))
                .andDo(MockMvcResultHandlers.print());

        MvcResult result = ra.andReturn();
        System.out.println("--------return: " + result.getResponse().getContentAsString());
    }
}

在测试过程中可以看到请求参数与响应参数:

file

11. 使用 DataJpaTest 测试

附录

附录A. 相关联的文章

附录B. 其他参考

附录C. Mock简要介绍

Mock简要介绍

  1. 什么是Mock
    在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。

  2. 为什么使用Mock对象
    使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
    在以下情况可以采用模拟对象来替代真实对象:

    • 真实对象的行为是不确定的(例如,当前的时间或温度);
    • 真实对象很难搭建起来;
    • 真实对象的行为很难触发(例如,网络错误);
    • 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
    • 真实的对象是用户界面,或包括用户界面在内;
    • 真实的对象使用了回调机制;
    • 真实对象可能还不存在;
    • 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。
  3. MockMvc
    MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
    接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。

  4. Mock中的一些方法的介绍

    • mockMvc.perform执行一个请求。
    • MockMvcRequestBuilders.get("XXX")构造一个请求。
    • ResultActions.param添加请求传值
    • ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
    • ResultActions.andExpect添加执行完成后的断言。
    • ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
      比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
    • ResultActions.andReturn表示执行完成后返回相应的结果。