在前面的课程中,我们构建了一个完整的REST API,它能够处理课程的创建、查询、更新和删除操作。然而,这个API有一个明显的局限性:所有数据都存储在内存中,一旦应用重启,之前创建的所有课程都会消失。 在实际的业务场景中,数据需要持久化保存,即使服务器重启或应用重新部署,数据也应该能够完整保留。这就是为什么我们需要引入数据库的原因。
数据库不仅仅是数据的存储容器,它还提供了事务管理、数据一致性保证、查询优化等企业级应用所需的核心能力。Spring Boot通过Spring Data JPA提供了与关系型数据库集成的优雅方式,它能够大大简化数据访问层的代码编写,让你能够专注于业务逻辑的实现,而不需要编写大量的SQL语句和JDBC样板代码。
这节课我们将把之前基于内存存储的课程管理系统改造为基于数据库的持久化系统。在这个过程中,你会看到如何添加数据库依赖、如何配置数据库连接、如何将普通的Java类转换为JPA实体、如何创建Repository接口、以及如何让服务层无缝地从内存存储切换到数据库存储。虽然我们使用的是H2内存数据库作为示例,但整个配置和代码结构对于MySQL、PostgreSQL等生产级数据库同样适用。
在开始编写数据库相关的代码之前,我们需要在项目的依赖配置中添加Spring Data JPA和数据库驱动的依赖。Spring Data JPA是Spring框架提供的用于简化数据访问的抽象层,它基于JPA(Java Persistence API)规范,能够自动生成查询方法,大大减少样板代码的编写。
打开项目根目录下的pom.xml文件,找到<dependencies>标签内的内容。在现有的spring-boot-starter-web依赖之后,添加以下两个依赖:
|<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <
spring-boot-starter-data-jpa起步依赖会自动引入Spring Data JPA、Hibernate(JPA的实现)、以及事务管理相关的依赖。h2依赖提供了H2数据库的JDBC驱动,这是一个纯Java实现的内存数据库,非常适合开发和测试场景。由于H2数据库驱动只在运行时需要,我们将其作用域设置为runtime,这样在编译时不会将其包含在类路径中。
添加完依赖后,在IDE中刷新Maven项目,让IDE重新加载依赖。在IntelliJ IDEA中,可以右键点击pom.xml文件,选择"Maven" -> "Reload Project";在Eclipse中,可以右键点击项目,选择"Maven" -> "Update Project"。等待依赖下载完成后,你就可以在代码中使用Spring Data JPA的功能了。

虽然H2是一个内存数据库,Spring Boot仍然需要知道如何连接到它。Spring Boot的自动配置机制会根据类路径中存在的依赖自动配置数据源,但对于H2这样的嵌入式数据库,我们还需要在配置文件中指定一些基本参数。
打开src/main/resources目录下的application.properties文件,如果这个文件不存在,就创建一个新文件。在文件中添加以下配置:
|# 数据库连接配置 spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # JPA配置 spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # H2控制台配置(开发时方便查看数据) spring.h2.console.enabled=true spring.h2.console.path=/h2-console
spring.datasource.url指定了数据库的连接URL,jdbc:h2:mem:testdb表示使用内存模式的H2数据库,数据库名为testdb。spring.datasource.driver-class-name指定了JDBC驱动的类名。spring.datasource.username和spring.datasource.password设置了数据库的用户名和密码,对于H2内存数据库,默认用户名是sa,密码可以为空。
spring.jpa.database-platform告诉Hibernate使用H2数据库的方言,这样Hibernate才能生成正确的SQL语句。spring.jpa.hibernate.ddl-auto=update是一个非常重要的配置,它让Hibernate在应用启动时自动检查实体类的定义,如果数据库表不存在就创建,如果表结构发生变化就更新。在生产环境中,这个值通常设置为validate或none,但在开发阶段使用update可以大大简化数据库表的创建过程。
spring.jpa.show-sql=true和spring.jpa.properties.hibernate.format_sql=true让Hibernate在控制台输出执行的SQL语句,这对于调试和了解框架的工作方式非常有帮助。spring.h2.console.enabled=true启用了H2数据库的控制台,你可以通过浏览器访问http://localhost:8080/h2-console来查看和管理数据库中的数据。
在前面的学习中,我们已经创建了一个简单的Course类作为数据模型。现在我们需要将这个类转换为JPA实体,让它能够映射到数据库表。JPA实体需要使用特定的注解来标记,这些注解告诉JPA框架如何将Java对象映射到数据库表结构。
打开src/main/java/com/example/myapp/my_spring_boot_app/model/Course.java文件,将内容替换为以下代码:
|package com.example.myapp.my_spring_boot_app.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "courses") public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Entity注解标记这个类是一个JPA实体,JPA框架会自动扫描所有带有这个注解的类,并为它们创建对应的数据库表。@Table(name = "courses")指定了数据库表的名称,如果不指定,JPA会使用类名作为表名(在这种情况下会是course,使用复数形式courses更符合常见的命名约定)。
@Id注解标记id字段为主键,@GeneratedValue(strategy = GenerationType.IDENTITY)表示主键的值由数据库自动生成,通常是通过自增序列或自增列来实现。这种策略在大多数关系型数据库中都有良好的支持,包括MySQL、PostgreSQL、H2等。
类的其他部分保持不变,JPA会自动将类中的字段映射到数据库表的列。字段名会直接映射到列名,如果字段名是驼峰命名(如courseTitle),JPA会将其转换为下划线命名(如course_title)。如果你需要自定义列名,可以使用@Column(name = "custom_column_name")注解。

在上一节,我们创建了一个InMemoryCourseRepository类来管理课程数据。现在我们需要用Spring Data JPA的Repository接口来替换它。Spring Data JPA的强大之处在于,你只需要定义一个接口,继承JpaRepository,框架就会自动为你实现所有基本的CRUD操作,包括保存、查询、更新、删除等。
在src/main/java/com/example/myapp/my_spring_boot_app/repository包下,创建一个新的接口CourseRepository,完整路径是src/main/java/com/example/myapp/my_spring_boot_app/repository/CourseRepository.java。在IDE中,找到repository包,右键点击,选择新建Java接口,输入名称CourseRepository,然后将内容替换为:
|package com.example.myapp.my_spring_boot_app.repository; import com.example.myapp.my_spring_boot_app.model.Course; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CourseRepository extends JpaRepository<Course, Long> { }
这个接口看起来非常简单,但它实际上提供了大量的方法。JpaRepository<Course, Long>中的第一个泛型参数Course是实体类型,第二个参数Long是主键的类型。通过继承这个接口,CourseRepository自动获得了save、findById、findAll、deleteById、delete、count等方法,这些方法可以直接使用,不需要编写任何实现代码。
@Repository注解虽然不是必须的(因为JpaRepository已经是一个Repository),但显式添加这个注解可以让代码的意图更加清晰,同时也便于Spring的组件扫描。Spring会在应用启动时自动创建这个接口的代理实现,并将其注册为Spring Bean,你可以在服务层中直接注入使用。
如果你需要自定义查询方法,Spring Data JPA还支持通过方法名自动生成查询。例如,如果你想根据标题查找课程,可以添加一个方法:
|Optional<Course> findByTitle(String title);
Spring Data JPA会根据方法名findByTitle自动生成查询语句,相当于SELECT * FROM courses WHERE title = ?。这种约定优于配置的方式大大减少了样板代码的编写。
现在我们已经有了JPA实体和Repository接口,接下来需要更新服务层,让它使用新的Repository而不是内存存储。由于CourseRepository接口的方法签名与之前的InMemoryCourseRepository非常相似,服务层的改动会相对较小。
打开src/main/java/com/example/myapp/my_spring_boot_app/service/CourseService.java文件,将内容替换为:
|package com.example.myapp.my_spring_boot_app.service; import com.example.myapp.my_spring_boot_app.model.Course; import com.example.myapp.my_spring_boot_app.repository.CourseRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.List; @Service @Transactional public class CourseService { private final CourseRepository repository; public CourseService(CourseRepository repository) { this
主要的改动包括将InMemoryCourseRepository替换为CourseRepository,以及将deleteById方法的返回值检查改为使用existsById方法。@Transactional注解被添加到了类级别,这意味着这个类中的所有方法都会在事务中执行。
当事务方法成功完成时,所有的数据库更改会被提交;如果方法抛出异常,所有的更改会被回滚,保证数据的一致性。
repository.save方法既可以用于插入新记录,也可以用于更新现有记录。当传入的实体对象的id为null时,JPA会将其视为新实体并执行插入操作;当id不为null且数据库中已存在对应记录时,JPA会执行更新操作。
既然我们已经使用了JPA Repository,之前创建的InMemoryCourseRepository类就不再需要了。你可以选择删除这个文件,或者将其重命名为InMemoryCourseRepository.old作为备份。
在IDE中,找到src/main/java/com/example/myapp/my_spring_boot_app/repository/InMemoryCourseRepository.java文件,右键点击,选择删除。
如果你担心删除后无法恢复,也可以先注释掉这个类的内容,或者将其移动到其他位置。但在生产代码中,保留不再使用的代码会增加维护成本,所以通常建议直接删除。
现在所有的代码都已经更新完成,让我们启动应用并验证数据库集成是否正常工作。在IDE中运行主应用类,或者使用Maven命令mvn spring-boot:run启动应用。
应用启动后,你应该能在控制台看到Hibernate创建表的SQL语句,类似这样:
|Hibernate: create table courses (id bigint generated by default as identity, description varchar(255), title varchar(255), primary key (id))
这表明Hibernate已经成功创建了courses表。现在你可以通过之前创建的REST API来测试数据库操作。使用curl或Postman发送POST请求创建课程:
|curl -X POST http://localhost:8080/api/courses \ -H "Content-Type: application/json" \ -d '{"title":"Spring Boot数据库集成","description":"学习如何使用Spring Data JPA"}'
然后发送GET请求查看所有课程:
|curl http://localhost:8080/api/courses
你应该能看到刚才创建的课程数据。更重要的是,即使你重启应用,这些数据仍然会保留在H2内存数据库中(直到应用完全关闭)。如果你使用的是文件模式的H2数据库(URL为jdbc:h2:file:./data/testdb),数据会持久化到磁盘文件中,即使应用重启也不会丢失。
你还可以通过H2控制台查看数据库中的数据。在浏览器中访问http://localhost:8080/h2-console,在登录页面中,JDBC URL填写jdbc:h2:mem:testdb,用户名填写sa,密码留空,然后点击连接。连接成功后,你可以执行SQL查询,例如SELECT * FROM courses,就能看到所有存储的课程数据。

在更新服务层代码时,我们添加了@Transactional注解。这个注解是Spring框架提供的事务管理机制的核心,它能够确保数据库操作的一致性。事务是一组数据库操作的逻辑单元,要么全部成功,要么全部失败,不会出现部分成功的情况。
例如,在updateCourse方法中,我们先查询现有课程,然后修改其属性,最后保存。如果在这个过程中发生任何错误(比如数据库连接中断、违反约束等),整个操作会被回滚,数据库会恢复到操作开始前的状态,不会出现数据不一致的情况。
Spring的事务管理支持多种传播行为,默认的REQUIRED行为表示如果当前存在事务就加入该事务,如果不存在就创建一个新事务。对于大多数场景,这个默认行为已经足够。如果你需要更细粒度的事务控制,可以在方法级别使用@Transactional注解,并指定不同的传播行为、隔离级别等参数。
虽然spring.jpa.hibernate.ddl-auto=update在开发阶段非常方便,但在生产环境中,手动管理数据库schema变更往往更加可控和安全。Flyway和Liquibase是两个流行的数据库迁移工具,它们能够通过版本化的SQL脚本或配置来管理数据库schema的变更。
这里我们简要介绍Flyway的使用。首先在pom.xml中添加Flyway依赖:
|<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency>
然后在src/main/resources/db/migration目录下创建SQL迁移脚本,文件名需要遵循特定的命名规则:V{version}__{description}.sql,例如V1__create_courses_table.sql:
|CREATE TABLE courses ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description VARCHAR(255) );
Flyway会在应用启动时自动执行这些迁移脚本,并记录执行历史,确保每个脚本只执行一次。这种方式让数据库schema的变更变得可追踪、可回滚,非常适合团队协作和生产环境部署。
虽然Hibernate的自动DDL功能在开发阶段非常方便,但在生产环境中,建议使用Flyway或Liquibase这样的数据库迁移工具来管理schema变更。这种方式不仅更加安全可控,还能让数据库变更历史变得可追踪,便于团队协作和问题排查。
通过这节课的学习,你已经成功地将应用从内存存储迁移到了数据库存储。你学会了如何添加Spring Data JPA依赖、如何配置数据库连接、如何将普通Java类转换为JPA实体、如何创建Repository接口、以及如何更新服务层代码。虽然我们使用的是H2内存数据库作为示例,但整个流程对于MySQL、PostgreSQL等生产级数据库同样适用,只需要修改application.properties中的数据库连接配置即可。
在下一节课中,我们将深入学习Spring Boot的配置管理机制,包括如何为不同环境配置不同的参数、如何使用配置属性类、以及如何通过Actuator监控应用状态。