Skip to main content

SpringBoot RestAPI + Spring Data JPA 实现数据库访问

以前用 Java 开发数据库交互的时候有用到 JDBC,JDBC 虽然很底层但是要自己组建 SQL,然后循环读取记录,实现一个简单的查询要花上不少工作量,然后就有了 ORM。

使用 JDBC (紫色区域是业务代码)

ORM 简单来说就是将查询结果,一条条映射成每个程序模型。程序不再需要关心记录长度,连接有没有掉线,等等维护数据库和查询记录的工作。程序只需要操作 ORM 返回的模型就可以了。

使用 ORM (紫色区域是业务代码)

JPA (Java Persistence API) 也是描述数据库的API,不过它只是作为一个 ORM 标准,有很多种工具实现这个标准。本次实验用的 JPA 实现是 Spring Data JPA。

SpringBoot Data JPA 相当于这个标准的实现,即 ORM 工具,它是 Spring 套件的子模块,配合 Spring 项目可以实现对数据库交互。

准备工具

本实验实现一个非常简单的档案应用,提供档案的增删查服务。

实验工具版本说明

工具 版本 说明
Gradle 7.2 依赖管理工具
springboot gradle plugin 2.4.8 SpringBoot Gradle 插件
springboot starter web 2.5.5 SpringBoot web 模块整合
springboot starter data jpa 2.5.2 SpringBoot Data JPA 模块整合
jdbc mysql connector java 8.0.21 JDBC Mysql 驱动
lombok 1.18.20 辅助开发工具包,推荐使用
Java 11 任意 Java 11 发行版,推荐 AdoptOpenJDK
docker 任意版本 虚拟化软件
mariadb docker 镜像 10.6.0 数据库镜像

准备一个数据库

数据库用 MySQL(mariaDB),运行在 docker 上,暴露 3306 端口。

用户名 root
密码 rootpw
地址 127.0.0.1:3306
数据库名 rootdb
数据表名 profiles

这里只展示数据库快速部署方法。

本实验部署 mariaDB

1. 启动 docker mariadb 容器,root密码rootpw,时区UTC+8

docker run --rm -d \
--name springboot_restapi_jpa_testdb \
-e MARIADB_ROOT_PASSWORD=rootpw \
-e TZ=Asia/Shanghai \
-p 3306:3306 \
mariadb:10.6.0

2. 进入容器cli,登录 root

docker exec -it springboot_restapi_jpa_testdb bash
mysql -u root --password=rootpw

3. 新建数据库 rootdb,建立数据表 profiles

CREATE DATABASE IF NOT EXISTS rootdb;
use rootdb;
CREATE TABLE IF NOT EXISTS `profiles` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(128),
  `slug` VARCHAR(128),
  `bio` VARCHAR(256),
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);

初始一个 SpringBoot RestAPI 项目

如果看过 SpringBoot 实验系列的文章,应该可以快速搭建项目。这里仍然会提供快速搭建的方法,但不会详细解释。

快速搭建 SpringBoot RestAPI 项目

1. Gradle 新建 Java Application 项目

gradle init

# 问项目类型: 选择 2(application)
# 问开发语言: 选择 3(Java)
# 问有无子项目: 选择 1(no)
# 问构建语言: 选择 1(Groovy)
# 问测试框架: 默认
# 问项目名称: 默认
# 问包名: org.example

2. app/build.gradle 添加 SpringBoot 插件

plugins {
  // ...

  // [添加] SpringBoot Gradle 插件
  id "org.springframework.boot" version "2.4.8"
}

3. app/build.gradle 添加依赖包

dependencies {
    // ...

    // [增加] SpringBoot Starter Web 模块整合包
    implementation 'org.springframework.boot:spring-boot-starter-web:2.5.5'
}

4. 程序入口类 app/src/main/java/org/example/App.java 初始化 Spring 应用

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

5. 测试启动,检查 SpringBoot 自带的 Tomcat 是否正常运行

./gradlew bootRun

接下来的实验都在上方新建的项目做操作。

添加 Spring Data JPA 依赖

Spring 模块化能给我们很多便利,实现某个功能,只需要引入对应的工具包(依赖)即可。

app/build.gradle 添加依赖

dependencies {

  // ...

  // [增加] JDBC mysql 驱动包
  implementation 'mysql:mysql-connector-java:8.0.21'

  // [增加] SpringBoot Starter Data JPA 模块整合包
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.5.2'

  // [增加] lombok 辅助开发工具包 & 辅助编译工具包
  implementation 'org.projectlombok:lombok:1.18.20'
  annotationProcessor 'org.projectlombok:lombok:1.18.20'
}

App 程序入口启用 Spring Data JPA

App 类前面必须声明 @EnableJpaRepositories,否则 Spring 找到 @Repository 会报错。

App 启用 Spring Data JPA
@SpringBootApplication
@EnableJpaRepositories // 启用 Spring Data JPA
public class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }
}

创建数据库模型 ProfileEntity

创建数据库模型的类,这个类应该有这些特点:

  • 声明 @Entity,ORM 识别为数据库模型

  • 声明 @Table,ORM 识别为数据表名

  • 描述数据库表的字段,作为成员变量。

  • 每个字段变量的 getter,setter 方法,供业务调用。

数据库模型 ProfileEntity.java
@Getter // lombok getter
@Setter // lombok setter
@NoArgsConstructor // lombok 自动生成构造方法 ProfileEntity()
@AllArgsConstructor // lombok 自动生成构造方法 ProfileEntity(...)
@ToString // lombok 自动生成 toString 方法
@Entity // JPA Entity,标记为数据库模型
@Table(name = "profiles") // JPA 指定该模型对应的表名
public class ProfileEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY) // 标记为 id 自动增长
  @Column(name = "id", unique = true, nullable = false)
  private long id;

  @Column(name="slug", unique = true, nullable = false)
  private String slug;

  // 什么@都不加也会当作一个 @Column,因为这个类是 JPA @Entity
  private String name;

  private String bio;

  @Column(name = "created_at")
  private Date createdAt;
}

上方创建的模型描述了一个档案,成员变量 id, slug, name, bio, created_at 都是数据表字段。

比如下方的 SQL 查询,ORM 可以将结果映射成多个 ProfileEntity 对象。

SELECT id, name, slug, bio, created_at FROM profiles;

创建数据仓库

数据仓库是提供对模型各种操作的接口,Spring Data JPA 会在这里自动生成 SQL。

Repo 只需要写接口就可以了,Spring Data JPA 会自动实现这些接口,如果不能实现,会报错。

数据仓库接口 ProfileRepo.java
public interface ProfileRepo extends JpaRepository<ProfileEntity, Long> {
  
  // Spring Data JPA 可自动实现 findByXXX
  Optional<ProfileEntity> findBySlug(String slug);
  Optional<ProfileEntity> findById(Long id);

  // JPQL 可以实现自定义 SQL,类似 SQL Statement
  @Query("SELECT profiles FROM ProfileEntity profiles WHERE profiles.id IN ( :idArray )")
  List<ProfileEntity> findByMultipleIds (@Param("idArray") Collection<Long> ids);
}

Spring Data JPA 支持 findByXXXX、findByBBBBAndCCCC 等多种命名

Query Creation | 官方文档 2.5.5

RestAPI 控制器与数据仓库交互

数据仓库封装的一套模型操作接口,就是为了让控制器不要直接通过 ORM 底层操作模型。

所以控制器接口相当于本实验开始时的 程序的工作量 了。

控制器 ProfileController.java
@RestController
public class ProfileController {
  
  @Autowired // 自动装配数据仓库
  private ProfileRepo profileRepo;


  // 处理 新建档案
  @Transactional(readOnly = true) // 每次调用该方法会启动事务
  @PostMapping("/profile")
  @ResponseStatus(HttpStatus.CREATED) // 201 Created
  public ProfileEntity handleCreate(@RequestBody ProfileEntity entity) {
    return profileRepo.save(entity); // 保存操作
  }

  // 处理 用 id 获取档案
  @Transactional(readOnly = true)
  @GetMapping("/profile")
  public ProfileEntity handleGetById(@RequestParam("id") Long id) {
    return profileRepo.findById(id).get(); // 查询操作
  }

  // 处理 用 slug 获取档案
  @Transactional(readOnly = true)
  @GetMapping("/profile/{slug}")
  public ProfileEntity handleGetBySlug(@PathVariable("slug") String slug) {
    return profileRepo.findBySlug(slug).get();
  }

  // 处理 用 id 取多份档案,用逗号分隔
  @Transactional(readOnly = true)
  public List<ProfileEntity> handleGetByMultipleIds (@RequestParam("id") String idStrings) {
    // "1,2,3" -> [1,2,3]
    String[] idSlice = idStrings.trim().split(",");
    Collection<Long> ids = (Collection<Long>) Arrays
      .asList(idSlice)
      .stream()
      .filter(idStr -> !idStr.isEmpty())
      .map(idStr -> Long.valueOf(idStr))
      .collect(Collectors.toList());
    return profileRepo.findByMultipleIds(ids); // JPQL 查询操作
  }

  // 处理 删除档案
  @Transactional
  public void handleDeleteById(long id) throws Exception {
    profileRepo.deleteById(id); // 删除操作
  }
}

App 配置数据源

application.yml(或 application.properties) 是 Spring App 的默认配置文件。如果存在则会读取该配置。

配置文件位置

在配置文件填写本实验开始时的数据库,用户名,密码。

配置文件 application.yml
server:
  port: 8080 # SpringBoot 端口,可以改

spring:
  # 配置数据源
  datasource:
    # mysql url,地址:本机3306,数据库rootdb,启用unicode,utf8避免乱码,关闭SSL加密,时区设置为UTC+8
    url: jdbc:mysql://127.0.0.1:3306/rootdb?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    # mysql 用户名
    username: root
    # mysql 密码
    password: rootpw
    # mysql 驱动路径
    driverClassName: com.mysql.cj.jdbc.Driver
  jpa:
    # 是否在操作数据库时,打印 sql 语句,debug 很有用
    show-sql: true

运行实验

启动应用

./gradlew bootRun
档案的增删查操作

增加档案

curl -XPOST \
-H 'Content-Type: application/json' \
-d '{"name":"Sam L", "slug":"luo", "bio": "the profile bio of Sam L"}' \
--url 'localhost:8080/profile'
# 结果 (201 Created)

获取档案

curl -XGET --url 'localhost:8080/profile?id=1'
# 结果 {"id":1,"name":"Sam L","slug":"luo","bio":"the profile bio of Sam L"}

用 slug 获取档案

curl -XGET --url 'localhost:8080/profile/luo'
# 结果 {"id":1,"name":"Sam L","slug":"luo","bio":"the profile bio of Sam L"}

JPQL - 取多份档案,用逗号分隔

curl -XGET --url 'localhost:8080/get_profiles_by_id?id=1,2'

删除档案

curl -XDELETE --url 'localhost:8080/profile?id=1'

附录与引用

定义了 Repo(数据仓库) 但是运行时没有创建对应的 Bean 导致 Autowired Repo 失败 (StackOverflow)

原因是没有在 App 声明 @EnableJpaRepositories

如果 Repo 在另一个包,Spring 也不会读到这个 Repo,所以 @EnableJpaRepositories 提供 basePackages 参数,可以传入外部包来导入外部的 Repo。

@EnableJpaRepositories(basePackages = {"项目 package", "外部 package"})
public class App { ... }

定义了 JPA @Entity 但是这个 Entity 在另一个包导致 Repo 无法匹配 type (StackOverflow)

同上面的不同 java 包问题,这个情况要用 @EntityScan 解决。

@EnableJpaRepositories(basePackages = {"项目 package", "外部 package"})
@EntityScan("外部 package")
public class App { ... }

自定义 Repo 的 SQL Query (即JPQL+@Query)

Spring Data JPA Custom Queries using @Query Annotation | Atta Blog

本实验项目归档(含步骤)

下载 | Download