SpringBoot RestAPI + Spring Data JPA 实现数据库访问
目录
以前用 Java 开发数据库交互的时候有用到 JDBC,JDBC 虽然很底层但是要自己组建 SQL,然后循环读取记录,实现一个简单的查询要花上不少工作量,然后就有了 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 |
这里只展示数据库快速部署方法。
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 实验系列的文章,应该可以快速搭建项目。这里仍然会提供快速搭建的方法,但不会详细解释。
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 会报错。
@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 方法,供业务调用。
@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 会自动实现这些接口,如果不能实现,会报错。
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 等多种命名
RestAPI 控制器与数据仓库交互
数据仓库封装的一套模型操作接口,就是为了让控制器不要直接通过 ORM 底层操作模型。
所以控制器接口相当于本实验开始时的 程序的工作量 了。
@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 的默认配置文件。如果存在则会读取该配置。
在配置文件填写本实验开始时的数据库,用户名,密码。
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