简介:以Spring Boot整合Hibernate与MySQL的入门级学习项目,面向初次接触Spring Boot或Hibernate的开发者,演示最基础的插入与查询流程。压缩包内含155个文件,总大小34.25MB,其中以64个jar依赖包为主,另有6个java源文件、2个jsp页面、3个properties配置及1个sql数据库脚本;sql脚本可直接建表,jar包已内置,配置好数据库连接后即可运行。项目保留了svn版本控制元数据(all-wcprops、entries、svn-base等),不影响正常使用。已有1826人学习下载。通过这个例子可以掌握Spring Boot+Hibernate的依赖管理、实体映射、Repository操作以及MySQL连接的典型写法,适合作为课堂作业或自学练手的起步模板。
1. Spring Boot + Hibernate + MySQL:“简单例子”背后其实有三层坑
很多人拿到 Spring Boot 的入门教程,看到“springboot+hibernate+mysql 简单例子”时,会觉得无非是建个项目、写个实体、跑起来就能连上数据库。但真到自己动手时,第一个报错往往不是业务代码,而是版本不匹配、驱动加载失败、时区差八个小时这类和环境相关的问题。这个组合之所以被大量生产项目采用,是因为 Spring Data JPA 把 Hibernate 的会话管理、事务边界和 Repository 封装得很干净,但不意味着配置可以随便写。这篇文章会把三件套的分工理清,再给出一套最小可运行例子的完整代码和参数说明,然后把最常踩的坑按现象、原因、解决列出来,最后落到一个调试习惯上。看完后你应该能照着复现,并且知道出了问题先看哪里。
2. 三件套的分工与选型:先想清楚谁管连接、谁管映射、谁管存储
2.1 为什么这个组合至今仍有大量生产环境在用
Spring Boot、Hibernate、MySQL 这三者其实各管一层:Spring Boot 负责应用装配和自动配置,Hibernate 负责对象与关系表的映射,MySQL 负责真正的数据存储。Spring Boot 官方默认的数据访问方案是 Spring Data JPA,而 Hibernate 是 JPA 规范最成熟、使用最广泛的实现。这个组合最大的优势在于:大部分 CRUD 不需要手写 SQL,实体类改完后可以交给 Hibernate 自动维护表结构,跨数据库迁移时只需要换方言配置,不用改业务代码。
另一个现实原因是团队协作成本。项目里如果有人熟悉 SQL、有人不熟悉,用 JPA 的 Repository 接口可以把查询收敛到方法名上,新成员上手速度快。相比之下,MyBatis 在复杂查询和 SQL 调优上有优势,但要求团队每个人都能写规范的 SQL,且 XML 映射文件多了之后维护成本不低。所以选型时不是比谁技术新,而是看项目里数据模型是否稳定、查询复杂度是否可控、团队更擅长写 Java 还是写 SQL。
我一般会建议:内部管理系统、业务模型以增删改查为主的场景,优先考虑 Hibernate;报表类、多表联查复杂、对 SQL 执行计划有强控需求的场景,再考虑 MyBatis。这个判断比争论框架优劣更重要。
2.2 JDBC、JPA、Hibernate 的关系:一次请求从浏览器到数据库的完整路径
理解这个组合最好的方式,是追踪一次 GET 请求的完整调用链路。浏览器请求打到 Controller,Controller 调用 Service,Service 调用 Repository 接口,Spring Data JPA 在运行时为这个接口生成代理实现,代理内部调用 EntityManager。EntityManager 是 JPA 规范里的核心接口,Hibernate 作为实现方,在这里把实体对象的状态变化翻译成 SQL 语句,再通过 JDBC 驱动发送给 MySQL。
这里有一条容易被忽略的边界:Spring Boot 默认集成的连接池是 HikariCP,它负责管理数据库连接的创建、复用和释放。数据源(DataSource)的配置在 application.yml 里,Hibernate 本身不直接管理连接,它通过 JDBC 从数据源借用连接。所以你在配置文件里看到的 spring.datasource 前缀下的参数,实际上大部分是 HikariCP 和 JDBC 驱动的配置,而 spring.jpa 前缀下的参数才是 Hibernate 的配置。
很多初次接触的人会把这两类配置混在一起调,结果出现连接超时就去改 Hibernate 的方言,出现 SQL 语法错误就去改连接池大小,这是方向上就错了。遇到问题先确认报错来自哪一层:驱动层、连接池层还是 Hibernate 层,再动对应的配置项。
3. 跑通最小可运行例子:从项目骨架到增删改查闭环
3.1 项目骨架与依赖版本选择
创建 Spring Boot 项目时,常见的做法是使用 Spring Initializr 生成基础骨架,勾选 Spring Web、Spring Data JPA、MySQL Driver 三个依赖。这三项会分别引入 Web 容器、JPA 自动配置和 MySQL JDBC 驱动,生成的 pom.xml 核心部分如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.x</version> <relativePath/> </parent> <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>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>这里第一行 parent 的作用是锁定 Spring Boot 所有依赖的版本号,你不需要单独给 Spring Data JPA 和 MySQL 驱动指定版本,避免出现 Hibernate 5 与 Spring Boot 3 不兼容这类问题。mysql-connector-j 的 scope 是 runtime,意味着它只在运行时需要,编译代码时不直接使用它。
版本选择上的一个血泪经验是:Spring Boot 2.x 对应 Hibernate 5.x,Spring Boot 3.x 对应 Hibernate 6.x,它们之间在配置项、方言类名、包结构上都有变化。比如 Hibernate 6 里不需要再配置 hibernate.dialect,它会根据 JDBC 连接的元数据自动检测数据库类型。如果照着网上一些旧教程在 Spring Boot 3 项目里手动指定 dialect 类,反而可能因为类名不对而启动失败。
3.2 数据源与 JPA 配置:application.yml 参数即使“默认能用”也要显式写清楚
生成骨架后,src/main/resources 下会有空的 application.properties,我习惯改成 application.yml,层级结构更清晰。一个最少配置如下:
spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: trueurl 里的参数每一条都有实际意义:useUnicode 和 characterEncoding 保证中文能正确存取,serverTimezone 设为 Asia/Shanghai 是因为 MySQL 8 默认时区与 JVM 所在时区可能不一致,不配置时程序读取时间字段经常比实际快或慢八个小时。useSSL=false 是避免本地开发时 SSL 握手带来的额外连接耗时和告警日志。
driver-class-name 在 MySQL 8 下固定为 com.mysql.cj.jdbc.Driver,如果项目用的是 MySQL 5.x,需要改成 com.mysql.jdbc.Driver。这里容易出问题的地方是,很多教程里用的是旧驱动类名,直接把代码拷到新环境就启动不了。判断依据很简单:MySQL 8 及以上的版本必须用 mysql-connector-j 8.x 提供的新驱动类。
ddl-auto 有四个可选值,这是 Hibernate 控制表结构的开关:none 表示完全不管表结构,启动时不建表也不检查;update 表示启动时对比实体和数据库,缺表则建表,缺列则加列,但不删除任何列;create 表示每次启动先删表再建表,数据会清空;create-drop 是 create 再加关闭时删表。开发环境用 update 最省事,生产环境应该用 none 或者交给专门的迁移工具管理。show-sql 和 format_sql 配合使用,前者的作用是决定是否在控制台打印 Hibernate 生成的 SQL,后者让打印出的 SQL 有缩进换行,可读性好很多。
3.3 实体映射与自动建表:先写代码,让表结构跟着实体走
配置完成后就可以写实体类。以下是一个最简的用户表实体,覆盖了主键策略、字段映射和表名指定三个最基本的问题:
package com.example.demo.entity; import jakarta.persistence.*; @Entity @Table(name = "sys_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", nullable = false, unique = true, length = 32) private String username; @Column(name = "password", nullable = false, length = 64) private String password; @Column(name = "nickname", length = 50) private String nickname; public User() { } public User(String username, String password, String nickname) { this.username = username; this.password = password; this.nickname = nickname; } // getter 和 setter 省略,实际代码里必须生成 }@Entity 是 JPA 规范里的注解,标识这是一个被 Hibernate 管理的实体类。@Table 的 name 属性指定对应的数据库表名,这里刻意写成 sys_user 而不是 user,是为了避开 user 在部分数据库版本里与系统表或保留字产生冲突。主键上的 @GeneratedValue(strategy = GenerationType.IDENTITY) 表示使用数据库自增主键,MySQL 下对应 AUTO_INCREMENT。
这里最典型的坑有两个。第一个是实体类必须提供无参构造方法。Hibernate 在查询返回结果时是通过反射创建实体实例的,它调用的是无参构造方法。如果你只写了带参构造方法,编译不会报错,但运行时查询会抛 InstantiationException。第二个坑是 @Column 的 nullable、unique、length 这些属性只有在 ddl-auto 为 update 或 create 时才会影响建表语句,不影响运行时校验。也就是说,这些属性是为了生成表结构服务的,不是用来做业务校验的。
启动应用后,Hibernate 会根据实体类自动创建 sys_user 表,不需要手动执行建表 SQL。但需要注意,它不会帮你创建数据库,localhost:3306/demo 里的 demo 这个 schema 必须先手动建好,否则连上去会报数据库不存在。
3.4 Repository 与 Service、Controller:增删改查最小闭环
有了实体后,下一步是数据访问层。Spring Data JPA 的 Repository 接口是这一层的主角,它不需要写实现类,框架会在启动时为它生成代理对象:
package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface UserRepository extends JpaRepository<User, Long> { List<User> findByUsername(String username); boolean existsByUsername(String username); }JpaRepository<User, Long> 里第一个泛型是实体类型,第二个是主键类型,这个接口已经内置了 save、findById、findAll、deleteById 等常用方法。findByUsername 这种以 findBy 开头的方法名会被 Spring Data JPA 解析成查询条件,不需要写实现。方法命名规则是框架的核心机制,常见的组合包括 findBy + 字段名、findBy + 字段名 + And + 字段名、existsBy 等,属性名必须和实体字段名严格一致。
Service 层负责事务和业务逻辑的编排。下面是用户模块最基础的服务实现,注册时会检查用户名是否已存在,再保存新用户:
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } @Transactional public User register(String username, String password, String nickname) { if (userRepository.existsByUsername(username)) { throw new RuntimeException("用户名已存在"); } User user = new User(username, password, nickname); return userRepository.save(user); } @Transactional(readOnly = true) public List<User> listAll() { return userRepository.findAll(); } }@Transactional 表示方法运行在事务里,默认情况下遇到 RuntimeException 会回滚,遇到受检异常不会回滚。register 方法里先查重再插入,如果第二次检查时发现用户名重复就抛出运行时异常,前面已经执行的数据库操作会全部回滚,不会留下脏数据。readOnly = true 是给 Hibernate 的优化提示,告诉它这个事务里只有查询,可以跳过脏检查机制,但注意这只是一个提示,不是强制约束。
Controller 是最终的外网入口。这里只写两个端点,一个用于注册,一个用于查询全部用户:
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping public User register(@RequestBody RegisterRequest request) { return userService.register(request.username(), request.password(), request.nickname()); } @GetMapping public List<User> listAll() { return userService.listAll(); } }到这里,一个最小可运行的增删改查闭环已经成立。启动 Spring Boot 应用后,访问 POST /api/users 会向 sys_user 表插入一条记录,访问 GET /api/users 会返回全部用户。整个过程里除了实体类、接口、两个 Java 类和一份 yml 配置,没有任何手写 SQL。
4. 常见排查与避坑:新手最常翻车的 5 个场景
4.1 现象:启动时报错 Cannot load driver class: com.mysql.jdbc.Driver
项目启动到数据源初始化阶段时抛出 Cannot load driver class,同时提示找不到 com.mysql.jdbc.Driver 这个类。原因是项目实际使用的是 mysql-connector-j 8.x,这个版本里旧的驱动类被移除了,新的类名是 com.mysql.cj.jdbc.Driver。网上大量帖子、教程里贴的还是旧类名,照抄后就必然报这个错。
解决方法是先确认 MySQL 版本,再决定驱动类名。MySQL 5.7 及以下用 com.mysql.jdbc.Driver,MySQL 8.0 及以上必须用 com.mysql.cj.jdbc.Driver。更稳妥的做法是直接删掉 application.yml 里的 driver-class-name 这一行,让 Spring Boot 根据 JDBC URL 自动推断驱动类,省掉这类烦恼。
4.2 现象:数据库表没建出来,日志里出现 Unknown database
启动日志显示 Hibernate 执行了建表语句,但报错 Unknown database。这里的根因是 ddl-auto 只能管理表,不能管理数据库实例本身。比如配置的 URL 是 jdbc:mysql://localhost:3306/demo,那么 demo 这个数据库必须提前存在,Hibernate 不会帮你创建它。
解决办法是先连接到 MySQL 服务端,执行 CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4;,然后再启动应用。注意字符集用 utf8mb4,因为它才完整支持中文和 emoji 字符,utf8 在 MySQL 里是 utf8mb3 的别名,遇到四字节字符会存不进去。
4.3 现象:写入数据库的时间比当前时间早了八个小时
插入记录后查询,发现 createTime 字段存的时间比本地实际时间早了八个小时。原因是 JDBC 连接串里没有指定 serverTimezone 参数,MySQL 服务端与 JVM 所在时区不同,驱动程序默认取服务端时区做转换。国内服务器常见的场景是服务端时区是 UTC,本地时区是东八区,出现正好八小时的偏差。
解决方法是往 URL 追加 serverTimezone=Asia/Shanghai,并同时把 MySQL 服务端的时区设置为与业务时区一致。这个参数在连接成功后还有一层影响:如果 MySQL 8 服务端本身配置了更大范围的时区表但没加载,启动日志会出现 The server time zone value is unrecognized 的告警,追加 useSSL=false 可以一并消除 SSL 告警。
4.4 现象:实体类写了带参构造方法后,启动正常但查询时报 InstantiationException
应用能启动,但第一次查询数据库就抛 org.hibernate.InstantiationException,提示无法实例化实体类。原因是 Hibernate 通过反射创建实体对象时,必须调用无参构造方法。类里如果没有显式声明任何构造方法,Java 会默认提供一个无参构造;但只要写了一个带参构造,默认的无参构造就会消失。
解决办法是在实体类里显式补一个无参构造方法,或者把带参构造去掉改用工厂方法创建对象。这个坑在代码编译阶段完全不会暴露,只有运行时才现形,属于典型的黑匣子问题。顺带一提,Lombok 的 @NoArgsConstructor 也能解决,但建议先搞懂原理再依赖工具。
4.5 现象:表名或字段名与数据库保留字冲突,建表 SQL 执行失败
建表时报语法错误,错误信息里能看到 order、user、desc 这类词被高亮。原因是 MySQL 的保留字列表比想象中宽,order、group、desc 都是保留字,直接作为表名或字段名会导致 DDL 语句语法错误。Hibernate 默认不会自动给表名和字段名加反引号。
解决办法是在 @Table 和 @Column 注解里显式指定名字,避开保留字。例如表名叫 sys_order、字段叫 order_status 而不是 order。如果实在无法改名,可以在命名策略里配置为所有标识符自动加反引号,但这是下策,会给后续 SQL 排查带来噪音。最好的策略是设计表时就不要用保留字,用具体的业务含义命名,既避免冲突,也提高可读性。
5. 进阶必调参数:从“能跑”到“跑得稳”的 4 个关键开关
5.1 事务边界:@Transactional 放在哪一层才不出错
最小例子里的 @Transactional 放在 Service 层方法上,这是最常见也是比较稳妥的位置。放在 Controller 层会扩大事务范围,导致数据库连接被占用的时间变长,影响并发能力;放在 Repository 层会让每个单一查询单独开启事务,业务上有多个写操作时,中途失败没有整体回滚机制,等于没控制事务。
我一般会遵循一个粗标准:一个事务方法里,要么只有一个写入,要么有多个写入但中间不允许出现可能导致回滚错误的远程调用。远程调用会占用数据库事务太长时间,极端情况会把连接池里的连接占满,是生产环境常见的坑。如果一个 Service 方法里既要写库又必须调用外部接口,就把外部调用放到事务提交之后,用 Spring 的事务同步机制或独立方法处理。
@Transactional 回滚规则默认只对 RuntimeException 生效,这点容易踩雷。比如 Service 方法里调了一个声明抛出 Exception 的方法,它抛出的受检异常不会触发回滚,数据照常提交。真出现这种情况时,需要显式声明 rollbackFor = Exception.class。
5.2 N+1 查询:为什么列表页数据量一大就慢到无法接受
N+1 查询的典型场景在关联实体上。一个 Department 实体关联多个 User,查询全部部门后,Hibernate 对每个部门执行一次用户查询,部门有 N 条时就有 N 次额外查询,加上初始的 1 次,共 N+1 次。三次以内的数据量看不出问题,到几百条就明显变慢。
解决方式有三种,按优先级排列:使用 @EntityGraph 在 Repository 查询方法上指定关联抓取策略;使用 JPA 的 join fetch 语法在 JPQL 里一次性查出关联对象;或者使用 Hibernate 的 @BatchSize 注解,把多批次查询合并为 in 查询。第一种最简洁,直接改接口方法就能生效,不侵入业务代码。
一个常见误区是打开 show-sql 后发现 SQL 数量多,就盲目给所有关联字段加 fetch = FetchType.EAGER。这会让无关查询也强制加载关联数据,反而更慢。正确的做法是保持默认的 LAZY 懒加载,只在真正需要关联数据的查询方法上用 @EntityGraph 指定。
5.3 分页与排序:getPage 方法背后的 SQL 长什么样
Spring Data JPA 内置的分页写法如下:
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id")); Page<User> result = userRepository.findAll(pageable);PageRequest.of 的三个参数分别表示页码(从 0 开始)、每页条数、排序规则。findAll(Pageable) 传入分页对象后,Spring Data JPA 会在内部生成 limit 语句。这里注意页码从 0 开始,前端传 1 时后端需要做 page - 1 处理,否则第二页数据拿到的是第一页。
排序字段名必须与实体属性名一致,而不是数据库列名。实体里叫 createTime,数据库列叫 create_time,排序就要写 createTime。改排序字段时最容易犯的错是把数据库列名拿来用,运行时报错提示找不到属性,原因就在这里。底层生成的 SQL 是 SELECT ... FROM sys_user ORDER BY id DESC LIMIT ?,limit 参数由 Pageable 解析生成,不需要手拼 SQL。
5.4 命名策略与字段映射:为什么实体写 createTime,表里却生成了 create_time
Hibernate 6 默认使用 SpringPhysicalNamingStrategy,它会把驼峰命名的字段自动转成下划线风格,实体里写 createTime,生成的表字段是 create_time。这个策略的规则是:连续小写字母保持原样,遇到大写字母时在前面加下划线并将大写转小写。userName 会变成 user_name,myURL 会变成 my_url。
如果没有这个自动转换,你的实体字段名和数据库列名必须完全一致才能映射成功,一致性要求高很多。Spring Boot 默认开启了这套策略,所以大部分项目不需要显式配置。但如果你用 @Column(name = "real_name") 显式指定了列名,这个显式指定优先于命名策略,Hibernate 会直接用你写的名字。
排查映射问题时的通用思路是:打开 SQL 日志,对比 Hibernate 生成的插入或查询语句里的列名与表里的实际列名是否一致。不一致时先看 @Column 是否写了 name,再确认命名策略是否被改动过,最后才考虑数据库列是否真的存在。
6. 把 SQL 日志打开:验证 Hibernate 行为的最终手段
本地开发时,我建议把 Hibernate 的 SQL 日志调到最详细等级。application.yml 里追加一段配置:
logging: level: org.hibernate.SQL: debug org.hibernate.orm.jdbc.bind: trace第一行让 Hibernate 在控制台打印它生成的每条 SQL,第二行打印 SQL 参数绑定的值。两行配合起来的效果是:你能看到某个方法实际执行了怎样的 SQL,参数是什么,执行顺序是什么。这是排查一切 ORM 行为不透明的起点。
紧跟着在 jpa.properties 里加上 format_sql: true,SQL 会按缩进分多行打印,可读性大幅提升。一个典型输出会长这样:
select u1_0.id, u1_0.username, u1_0.password, u1_0.nickname from sys_user u1_0按这个方法验证上面章节的例子,你会发现 register 方法先执行了一条 select 查用户名是否存在,再执行一条 insert 插入数据,两条 SQL 都在同一个事务里。如果第二条失败,第一条所在的事务也会回滚,这正是事务边界配置正确的表现。
除了 SQL 日志,还可以在 debug 级别看到事务的开合信息。Hibernate 会在每次事务开始时输出 Transaction start,提交时输出 Transaction committed,回滚时输出 Transaction rollback。配合批处理时,你也能看到真的把多条 insert 语句合并成一条 JDBC 批处理,还是逐条发送。这些都是框架的黑匣子,打开日志就能看透。
这个习惯帮我解决过很多次“数据没保存上”“查询多了几条”“时间不对”的疑难问题。每次动手改代码前,先看日志确认当前实现实际做了什么,改完再看日志确认结果是否符合预期。如果你没有经历过被一条隐藏的 SQL 折磨到翻车的过程,可能体会不到这一步的价值;但一旦经历过,就会明白日志是你和 ORM 之间唯一可靠的沟通方式。希望这篇从最小例子到参数细节的拆解,能让你后续遇到问题时先想到去验证,而不是靠猜。希望帮到你。
本文还有配套的精品资源,点击获取