Spring Boot 入门 Day 4:JPA + H2 数据库持久化,数据告别「重启就没了」
Spring Boot 入门 Day 4:JPA + H2 数据库持久化,数据告别「重启就没了」
从内存 Map 到真实数据库,让你的数据「落地」
问题:Day 3 的「假存储」
Day 3 我们完成了完整的 CRUD,但数据存在 ConcurrentHashMap 里。这意味着:
- ✅ 程序运行时:数据正常
- ❌ 重启项目:数据全没了
就像一个记事本写在草稿纸上,一关机字迹全消失。今天我们要把数据真正「存进硬盘」。
目标:数据库持久化
Day 3:请求 → Controller → Service → ConcurrentHashMap(内存,重启丢失)
Day 4:请求 → Controller → Service → Repository → H2 数据库(磁盘,永久保存)
核心概念速览
1. ORM — 用操作对象的方式操作数据库
传统 JDBC 方式查一条记录要写 20 行:
Connection conn = ...;
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM tasks WHERE id = ?");
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
Task task = new Task();
task.setId(rs.getLong("id"));
task.setTitle(rs.getString("title"));
// ... 还有很多行
用 JPA(ORM 框架)只要 1 行:
Task task = taskRepository.findById(id).orElseThrow();
ORM = Object-Relational Mapping(对象关系映射),让你用 Java 对象的方式操作数据库,框架帮你翻译成 SQL。
2. H2 数据库 — 开发神器
H2 是一个纯 Java 写的嵌入式数据库:
| 特点 | 说明 |
|---|---|
| 零安装 | 一个 jar 包搞定,不需要装 MySQL |
| 文件模式 | 数据存到磁盘文件,重启不丢 |
| Web 控制台 | 自带浏览器管理界面 /h2-console |
| 完美兼容 | 支持标准 SQL,后续换 MySQL 几乎不用改代码 |
3. JPA 核心注解一览
| 注解 | 作用 | 放哪 |
|---|---|---|
@Entity |
标记这是数据库表对应的实体类 | 类上 |
@Table(name="xxx") |
指定表名 | 类上 |
@Id |
主键 | 字段上 |
@GeneratedValue |
主键自动生成策略 | 字段上 |
@Column |
配置列名、长度、是否可空 | 字段上 |
@PrePersist |
插入数据库前自动执行的方法 | 方法上 |
4. Spring Data JPA 的魔法
你只需要写一个接口,不用写实现类:
public interface TaskRepository extends JpaRepository<Task, Long> {
// 继承后自动获得:findAll、findById、save、deleteById...
// 按方法名命名规则自动生成查询:
List<Task> findByStatus(String status);
// → SELECT * FROM tasks WHERE status = ?
List<Task> findByPriorityOrderByCreatedAtDesc(String priority);
// → SELECT * FROM tasks WHERE priority = ? ORDER BY created_at DESC
}
方法名即 SQL!Spring 在运行时自动解析方法名并生成实现类。
5. 事务(@Transactional)
保证多个数据库操作要么全成功,要么全失败:
转账场景:
A 账户 -1000 ✅
系统断电 💥
B 账户 +1000 ❌ 没执行
→ 事务回滚,A 的 1000 块退回
@Transactional— 写操作默认事务@Transactional(readOnly = true)— 读操作标记只读,性能更好
动手实战:4 步完成改造
Step 1:添加依赖
在 pom.xml 的 <dependencies> 中加入:
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Step 2:配置数据库
修改 src/main/resources/application.yml:
spring:
datasource:
url: jdbc:h2:file:./data/taskdb # 文件模式,数据存磁盘
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true # 开启 Web 控制台
path: /h2-console # 访问路径
jpa:
hibernate:
ddl-auto: update # 自动建表/更新表结构
show-sql: true # 控制台打印 SQL
properties:
hibernate:
format_sql: true # SQL 格式化
ddl-auto 四个值选哪个?
| 值 | 行为 | 什么时候用 |
|---|---|---|
update |
自动建表,实体变自动更新 | ✅ 开发阶段 |
create-drop |
启动建表,停止删表 | 测试 |
create |
每次启动重建表(会删旧表!) | 全新项目 |
validate |
只校验不修改 | ✅ 生产环境 |
Step 3:改造 Task 实体
给 Task.java 加上 JPA 注解:
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Column(length = 500)
private String description;
@Column(nullable = false)
private String status = "TODO";
@Column(nullable = false)
private String priority = "MEDIUM";
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
// JPA 要求有无参构造器
public Task() {}
// 插入数据库前自动设时间
@PrePersist
protected void onCreate() {
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
}
// Getter / Setter ...
}
关键变化:
@GeneratedValue→ ID 由数据库自动生成,不用手动管了@PrePersist→ 创建时间在实体内部自动设置updatable = false→ 创建时间写入后不可更改
Step 4:创建 Repository + 改造 Service
新建 src/main/java/com/agent/repository/TaskRepository.java:
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByStatus(String status);
List<Task> findByStatusOrderByCreatedAtDesc(String status);
List<Task> findByPriorityOrderByCreatedAtDesc(String priority);
}
改造 TaskService.java——删掉 ConcurrentHashMap,换成 Repository:
@Service
@Transactional
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional(readOnly = true)
public List<Task> listAll(String statusFilter) {
if (statusFilter != null && !statusFilter.isBlank()) {
return taskRepository.findByStatusOrderByCreatedAtDesc(statusFilter);
}
return taskRepository.findAll();
}
@Transactional(readOnly = true)
public Optional<Task> getById(Long id) {
return taskRepository.findById(id);
}
public Task create(Task task) {
return taskRepository.save(task); // 从 8 行变 1 行!
}
public Optional<Task> update(Long id, Task updateData) {
return taskRepository.findById(id).map(existing -> {
if (updateData.getTitle() != null) existing.setTitle(updateData.getTitle());
if (updateData.getDescription() != null) existing.setDescription(updateData.getDescription());
if (updateData.getStatus() != null) existing.setStatus(updateData.getStatus());
if (updateData.getPriority() != null) existing.setPriority(updateData.getPriority());
return taskRepository.save(existing);
});
}
public boolean delete(Long id) {
if (taskRepository.existsById(id)) {
taskRepository.deleteById(id);
return true;
}
return false;
}
}
Service 变化对比:
| Day 3(内存) | Day 4(数据库) | |
|---|---|---|
| 存储 | ConcurrentHashMap |
TaskRepository |
| 创建 | 8 行(设 ID、设时间、放 Map) | 1 行 save() |
| ID 管理 | 手动 AtomicLong |
数据库自动生成 |
| 时间管理 | Service 手动设 | @PrePersist 自动 |
验证:重启数据还在!
mvn spring-boot:run
启动后浏览器打开 http://localhost:8080/h2-console,JDBC URL 填 jdbc:h2:file:./data/taskdb,点 Connect 就能看到 TASKS 表。
# 1. 创建一条任务
Invoke-WebRequest -Uri http://localhost:8080/api/tasks `
-Method Post -ContentType "application/json" `
-Body '{"title":"持久化测试","description":"重启后应该还在","priority":"HIGH"}'
# 2. 查列表 — 确认创建成功
Invoke-WebRequest -Uri http://localhost:8080/api/tasks
# 3. Ctrl+C 停掉项目,再 mvn spring-boot:run 重启动
# 4. 再查列表 — 数据还在!✅
Invoke-WebRequest -Uri http://localhost:8080/api/tasks
Day 4 总结
| 要点 | 一句话 |
|---|---|
| JPA | 操作对象 = 操作数据库,告别手写 SQL |
| Spring Data JPA | 只写接口不写实现,框架自动生成 |
| H2 数据库 | 零安装嵌入式数据库,开发利器 |
ddl-auto: update |
自动同步实体和表结构 |
@Transactional |
保证数据一致性,读写分离优化性能 |
| Repository 方法命名 | findByXxxOrderByYyyDesc 直接翻译成 SQL |
一句话记住 Day 4:内存变数据库,重启不丢数据。你写 interface,Spring 写实现。
下一篇 Day 5:统一响应格式 + 全局异常处理 + DTO 分层,让 API 更专业。
更多推荐

所有评论(0)