☰
Spring Boot+Hibernate+MySQL事务闭环实战
2026/11/5 5:41:11 网站建设 项目流程

简介:这是一份面向Java初学者与Spring Boot入门开发者的轻量级实战项目,聚焦ORM框架集成与数据库基础操作,帮助学习者快速掌握Spring Boot整合Hibernate访问MySQL的核心流程。资源包含完整的可运行工程,已预置全部依赖JAR包及初始化SQL脚本,仅需创建对应数据库并执行脚本即可启动项目,完成用户数据的插入与查询演示。压缩包共155个文件,主体为64个JAR包(支撑Spring Boot与Hibernate运行)、6个Java源文件(含实体类、Repository与Controller)、3个properties配置文件(含数据库连接与JPA设置)以及1个SQL建表脚本,结构简洁、依赖内聚,便于理解项目骨架与配置逻辑。目前已有1829人学习下载,适合零基础接触Spring生态、希望跳过环境搭建陷阱、直接切入CRUD实践的学习者,所见即所得地掌握从配置到执行的完整链路。

1. Spring Boot + Hibernate + MySQL 简单例子:不是“Hello World”,而是能跑通事务、查出真实数据、改完立刻生效的最小可验证闭环

你搜到这个资源时,大概率正卡在这样一个黑匣子时刻:Spring Boot 启动成功,控制台刷了一堆Starting Servlet WebServer,但@RestController返回空 JSON;或者@Service里调repository.save()没报错,MySQL 表里却死活没新记录;又或者@Transactional加了,日志里也显示begin transaction,但异常抛出后数据还是留在库里——这不是代码写错了,是整个 ORM 层和数据库连接的握手没对上。这个「简单例子」不是教你怎么写注解,而是把 Spring Boot 的自动配置、Hibernate 的 Session 生命周期、MySQL 的事务隔离级别这三股绳拧成一股能拉得动真实业务的线。它面向的是刚从纯 JDBC 或 MyBatis 切过来、对SessionFactory和EntityManager还分不清,但手头有个小需求必须三天内联调通的开发者。它不讲 JPA 规范史,只告诉你spring.jpa.hibernate.ddl-auto=update在开发期怎么用才不翻车,@GeneratedValue(strategy = GenerationType.IDENTITY)为什么在 MySQL 8.0+ 下必须配serverTimezone=UTC才不报错,以及——最关键的一点——如何用一条curl命令验证你写的@PostMapping真正把数据落进了磁盘,而不是停在一级缓存里。


2. 项目结构与核心依赖:为什么只选spring-boot-starter-data-jpa而不是spring-boot-starter-jdbc

2.1 依赖选型:避开hibernate-core手动引入的坑

这个例子严格使用 Spring Boot 官方 Starter 机制,而非手动引入hibernate-core或mysql-connector-java。原因很实际:Spring Boot 2.7.x(本例基于此)已将 Hibernate 5.6.x 与 MySQL 8.0 驱动深度对齐,手动指定版本极易触发ClassCastException: org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator这类底层类加载冲突。正确做法是在pom.xml中声明:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

提示:spring-boot-starter-data-jpa内置了hibernate-core、spring-orm、spring-jdbc三者协调版本,且自动注册LocalContainerEntityManagerFactoryBean。手动加hibernate-core会破坏这一契约,导致@EnableJpaRepositories失效或EntityManager注入为 null。

2.2 目录结构:实体、仓库、服务、控制器四层必须物理隔离

项目采用标准分层,拒绝“全塞进一个包”的新手写法。目录结构强制如下:

src/main/java/com/example/demo/ ├── DemoApplication.java // 启动类,必须在根包下 ├── entity/ // @Entity 类专属包 │ └── User.java ├── repository/ // 继承 JpaRepository 的接口 │ └── UserRepository.java ├── service/ // @Service 实现类 │ ├── UserService.java │ └── UserServiceImpl.java └── controller/ // @RestController └── UserController.java

这种结构不是为了“看起来规范”,而是让 Spring Boot 的组件扫描(@ComponentScan)能精准定位:@Entity必须被LocalContainerEntityManagerFactoryBean扫描到,JpaRepository接口必须被JpaRepositoryFactoryBean代理生成实现类,@Service必须被Autowired正确注入。若把User.java放进controller包,@EntityScan默认不扫该路径,启动时不会报错,但运行时save()永远返回null。

2.3application.yml关键配置:每行都是血泪经验

以下配置是本例能跑通的最小必要集,删掉任意一行都可能触发静默失败:

spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect database-platform: mysql main: allow-bean-definition-overriding: true
  • serverTimezone=UTC:MySQL 8.0+ 默认时区为SYSTEM,Java 8+ 的LocalDateTime无时区概念,不显式指定会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized;
  • allow-bean-definition-overriding: true:当多个@Configuration类定义同名 Bean 时(如测试环境覆盖数据源),避免BeanDefinitionOverrideException;
  • dialect: MySQL8Dialect:强制 Hibernate 使用 MySQL 8 专用方言,否则JSON类型字段、ROW_NUMBER()窗口函数等特性无法识别;
  • ddl-auto: update:开发期唯一安全选项,create会清空表,validate不建表,none要求表必须预先存在。

3. 核心代码实现:从实体定义到 REST 接口的完整链路

3.1User.java:@Id+@GeneratedValue的精确写法

MySQL 主键自增必须与数据库引擎强绑定,InnoDB 是唯一选择。实体类需严格匹配:

package com.example.demo.entity; import javax.persistence.*; @Entity @Table(name = "t_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 关键:必须用 IDENTITY,不能用 AUTO @Column(name = "id", nullable = false) private Long id; @Column(name = "username", length = 50, nullable = false) private String username; @Column(name = "email", length = 100) private String email; // 必须有无参构造函数(JPA 反射要求) public User() {} // 全参构造函数(方便测试) public User(String username, String email) { this.username = username; this.email = email; } // getter/setter 省略,但实际项目中必须完整 }

参数说明:GenerationType.IDENTITY对应 MySQL 的AUTO_INCREMENT,Hibernate 会执行INSERT INTO t_user (username, email) VALUES (?, ?)后调用SELECT LAST_INSERT_ID()获取主键。若误用GenerationType.AUTO,Hibernate 可能生成SELECT next_val FROM hibernate_sequence,而 MySQL 并无此表,导致Table 'demo_db.hibernate_sequence' doesn't exist错误。

3.2UserRepository.java:继承JpaRepository的零配置能力

JPA Repository 不需要写 SQL,但方法命名必须遵循规范:

package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository // 显式标注,增强可读性 public interface UserRepository extends JpaRepository<User, Long> { // Spring Data JPA 自动解析方法名 User findByUsername(String username); // 生成 SELECT * FROM t_user WHERE username = ? User findFirstByEmailOrderByUsernameDesc(String email); // 生成 LIMIT 1 + ORDER BY }

逻辑说明:JpaRepository<User, Long>的第二个泛型Long必须与User.id类型一致。若id是Integer,此处写Long会导致IllegalArgumentException: Parameter value [1] did not match expected type [java.lang.Integer]。@Repository虽非必需(Spring Boot 会自动扫描),但显式声明能让 IDE 在@Autowired UserRepository时提供更准的类型提示。

3.3UserService:事务边界与异常传播的实操控制

服务层是事务控制的核心,必须明确@Transactional的作用域:

package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Autowired public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override @Transactional // 此注解必须在 public 方法上,且由 Spring 代理调用才生效 public User createUser(String username, String email) { User user = new User(username, email); return userRepository.save(user); // save() 在事务内执行 INSERT } @Override @Transactional(readOnly = true) // 查询方法加 readOnly 提升性能 public List<User> findAllUsers() { return userRepository.findAll(); } @Override @Transactional public void deleteUser(Long id) { userRepository.deleteById(id); // DELETE 语句在此事务中执行 } }

关键细节:@Transactional默认传播行为是REQUIRED,即“有则加入,无则新建”。若createUser()内部调用另一个@Transactional方法,两者共享同一事务。readOnly = true会禁用 Hibernate 一级缓存的脏检查,减少UPDATE语句生成,对纯查询提升 10%~15% 性能。

3.4UserController.java:REST 接口与 HTTP 状态码的严谨对应

控制器需将业务异常转化为 HTTP 状态码,而非返回 200 + error message:

package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @PostMapping public ResponseEntity<User> createUser(@RequestBody User user) { // 检查必填字段 if (user.getUsername() == null || user.getUsername().trim().isEmpty()) { return ResponseEntity.badRequest().build(); // 400 } User savedUser = userService.createUser(user.getUsername(), user.getEmail()); return ResponseEntity.status(HttpStatus.CREATED).body(savedUser); // 201 } @GetMapping public ResponseEntity<List<User>> getAllUsers() { List<User> users = userService.findAllUsers(); return ResponseEntity.ok(users); // 200 } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { try { userService.deleteUser(id); return ResponseEntity.noContent().build(); // 204 } catch (Exception e) { return ResponseEntity.notFound().build(); // 404 } } }

参数说明:@RequestBody将 JSON 自动反序列化为User对象,依赖 Jackson。若前端传{"username":"test"}而User构造函数未处理null email,save()仍会成功(因@Column(nullable = true)),但@DeleteMapping的@PathVariable Long id若传字符串(如"abc"),Spring MVC 会直接返回 400,无需手动校验。


4. 启动与验证:用 curl 和 MySQL CLI 交叉验证数据落地

4.1 启动应用并观察关键日志

运行DemoApplication.main()后,控制台必须出现以下三类日志,缺一不可:

  1. 数据源初始化日志(证明 MySQL 连接成功):
    HikariPool-1 - Starting...→HikariPool-1 - Start completed.
    若卡在Starting...,检查url中的localhost:3306是否可达,或 MySQL 服务是否运行。

  2. Hibernate DDL 日志(证明表已创建):
    Hibernate: drop table if exists t_user
    Hibernate: create table t_user ...
    若无create table,检查spring.jpa.hibernate.ddl-auto=update是否拼写错误,或@Entity类未被扫描到。

  3. JPA Repository 初始化日志(证明仓库可用):
    JpaRepositoryFactoryBean: Creating repository instance for ... UserRepository
    若缺失,检查UserRepository是否在@SpringBootApplication扫描路径下。

4.2 用 curl 发起真实请求并验证响应

在终端执行以下命令,顺序不能乱:

# 1. 创建用户(POST) curl -X POST http://localhost:8080/api/users \ -H "Content-Type: application/json" \ -d '{"username":"alice","email":"alice@example.com"}' # 预期响应:HTTP 201 + JSON body 包含 id 字段 # {"id":1,"username":"alice","email":"alice@example.com"} # 2. 查询所有用户(GET) curl http://localhost:8080/api/users # 预期响应:HTTP 200 + 数组 # [{"id":1,"username":"alice","email":"alice@example.com"}] # 3. 删除用户(DELETE) curl -X DELETE http://localhost:8080/api/users/1 # 预期响应:HTTP 204(无响应体)

验证逻辑:curl命令必须返回明确的 HTTP 状态码。若POST返回 200 而非 201,说明ResponseEntity.status(HttpStatus.CREATED)未生效;若GET返回空数组[],但POST成功,说明数据未落库,需立即检查 MySQL。

4.3 用 MySQL CLI 直连验证数据一致性

打开 MySQL 命令行,执行:

-- 1. 连接数据库 mysql -u root -p -- 2. 选择数据库(确保 application.yml 中的 demo_db 已存在) USE demo_db; -- 3. 查看表结构(确认 Hibernate 创建的字段与 @Column 一致) DESCRIBE t_user; -- 4. 查询数据(必须看到与 curl POST 相同的记录) SELECT * FROM t_user; -- 5. 手动插入一条记录,验证双向同步 INSERT INTO t_user (username, email) VALUES ('bob', 'bob@example.com'); -- 6. 用 curl GET 验证:此时 /api/users 应返回两条记录

参数说明:DESCRIBE t_user输出中,id字段的Extra列必须为auto_increment,否则@GeneratedValue(strategy = GenerationType.IDENTITY)失效;username的Null列必须为NO,对应@Column(nullable = false)。若DESCRIBE显示Null=YES,说明@Column(nullable = false)未被 Hibernate 解析,常见原因是@Entity类未被扫描或@Column拼写错误。


5. 避坑指南:五个高频静默失败场景与根因修复

5.1 现象:userRepository.save()返回对象的id为null

原因:MySQL 表的id字段未设为AUTO_INCREMENT,或@Id+@GeneratedValue注解未同时存在。Hibernate 执行INSERT后无法获取主键值。
解决:

  1. 登录 MySQL,执行SHOW CREATE TABLE t_user;,确认id字段含AUTO_INCREMENT;
  2. 检查User.java中@Id和@GeneratedValue(strategy = GenerationType.IDENTITY)是否在同一字段上;
  3. 若使用@SequenceGenerator,MySQL 不支持,必须换为IDENTITY。

5.2 现象:@Transactional无效,异常后数据仍保留在库中

原因:@Transactional方法被同类内其他方法直接调用(如A.method1()调用A.method2()),绕过 Spring 代理,事务不生效。
解决:

  1. 确保事务方法被外部 Bean 调用(如 Controller 调 Service);
  2. 若必须同类调用,用ApplicationContext获取自身代理:((UserService) applicationContext.getBean(UserService.class)).deleteUser(id);
  3. 在application.yml中添加spring.aop.proxy-target-class=true强制 CGLIB 代理。

5.3 现象:findByUsername("alice")返回null,但 MySQL 中存在该记录

原因:Hibernate 一级缓存(Session 级别)未刷新,或@Query方法未加@Modifying注解。
解决:

  1. 在UserRepository中添加@Query("SELECT u FROM User u WHERE u.username = :name")时,若该方法用于更新,必须加@Modifying;
  2. 查询前手动清除缓存:entityManager.clear()(不推荐,影响性能);
  3. 更可靠方式:在UserService中用@Transactional(readOnly = true)包裹查询,确保每次查询走数据库。

5.4 现象:启动时报java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext

原因:JDK 11+ 移除了 JAXB API,而旧版 Hibernate 依赖它。
解决:

  1. 在pom.xml中添加 JAXB 依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>
  1. 或降级至 JDK 8(开发环境推荐,避免兼容性问题)。

5.5 现象:curl POST返回 400,但控制台无任何错误日志

原因:@RequestBody反序列化失败,Jackson 找不到User的无参构造函数或 setter 方法。
解决:

  1. 确认User.java有public User() {};
  2. 确认所有字段有public void setXxx()方法(IDE 可自动生成);
  3. 在application.yml中添加spring.jackson.deserialization.fail-on-unknown-properties=false临时跳过未知字段。

6. 进阶验证技巧:用 H2 内存数据库做单元测试,彻底隔离 MySQL 依赖

6.1 添加 H2 依赖并配置测试专用 Profile

在pom.xml中添加test范围的 H2:

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency>

创建src/test/resources/application-test.yml:

spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop h2: console: enabled: true

6.2 编写UserServiceTest:验证事务回滚与数据一致性

测试类需启用@DataJpaTest(仅加载 JPA 相关 Bean)和@Import测试配置:

package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.*; @DataJpaTest @Import(TestConfig.class) // 加载 application-test.yml @ActiveProfiles("test") class UserServiceTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void testCreateUser_WithRollbackOnException() { // Given User user = new User("test", "test@example.com"); // When User savedUser = userRepository.save(user); // Then assertNotNull(savedUser.getId()); // 主键已生成 assertEquals("test", savedUser.getUsername()); // 验证数据已写入内存数据库 User found = userRepository.findByUsername("test"); assertNotNull(found); assertEquals("test@example.com", found.getEmail()); } }

验证逻辑:@DataJpaTest会自动配置 H2 内存库,并在每个@Test方法后执行TRUNCATE,确保测试间无状态污染。TestEntityManager提供flush()和clear()方法,可精确控制 Hibernate Session 行为,比@Transactional更细粒度。

6.3 用 H2 Console 实时查看内存库状态

启动测试后,访问http://localhost:8080/h2-console(H2 控制台默认开启),输入:

  • JDBC URL:jdbc:h2:mem:testdb
  • Username:sa
  • Password: 空

即可像操作 MySQL 一样执行SELECT * FROM t_user;,实时验证save()是否真正触发了 INSERT 语句。这是比日志更直观的“数据落地”证据。

6.4 从那以后我每次写 JPA 代码,都强制走一遍这三步验证

第一步:curl POST后立刻SELECT * FROM t_user,确认数据在 MySQL 里;
第二步:curl GET返回的数据与 MySQL 查询结果逐字段比对,排除 Jackson 序列化干扰;
第三步:写一个@DataJpaTest,用TestEntityManager的flush()强制同步,再find()验证缓存一致性。
这三步做完,我才敢把代码推到 Git。因为 Hibernate 的缓存、事务、延迟加载像一层毛玻璃,不亲手擦干净,永远不知道后面是清晰的业务逻辑,还是一团模糊的 bug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询