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 更专业。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐