多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Spring Boot + Hibernate + MySQL 整合实战:配置、踩坑与性能优化

Spring Boot + Hibernate + MySQL 整合实战:配置、踩坑与性能优化 简介以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 BootHibernate的依赖管理、实体映射、Repository操作以及MySQL连接的典型写法适合作为课堂作业或自学练手的起步模板。1. Spring Boot Hibernate MySQL“简单例子”背后其实有三层坑很多人拿到 Spring Boot 的入门教程看到“springboothibernatemysql 简单例子”时会觉得无非是建个项目、写个实体、跑起来就能连上数据库。但真到自己动手时第一个报错往往不是业务代码而是版本不匹配、驱动加载失败、时区差八个小时这类和环境相关的问题。这个组合之所以被大量生产项目采用是因为 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 请求的完整调用链路。浏览器请求打到 ControllerController 调用 ServiceService 调用 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.x/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/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.xSpring 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse 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 所在时区可能不一致不配置时程序读取时间字段经常比实际快或慢八个小时。useSSLfalse 是避免本地开发时 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 JpaRepositoryUser, Long { ListUser findByUsername(String username); boolean existsByUsername(String username); }JpaRepositoryUser, 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 ListUser 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 ListUser 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.DriverMySQL 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 追加 serverTimezoneAsia/Shanghai并同时把 MySQL 服务端的时区设置为与业务时区一致。这个参数在连接成功后还有一层影响如果 MySQL 8 服务端本身配置了更大范围的时区表但没加载启动日志会出现 The server time zone value is unrecognized 的告警追加 useSSLfalse 可以一并消除 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 N1 查询为什么列表页数据量一大就慢到无法接受N1 查询的典型场景在关联实体上。一个 Department 实体关联多个 User查询全部部门后Hibernate 对每个部门执行一次用户查询部门有 N 条时就有 N 次额外查询加上初始的 1 次共 N1 次。三次以内的数据量看不出问题到几百条就明显变慢。解决方式有三种按优先级排列使用 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)); PageUser 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_timeHibernate 6 默认使用 SpringPhysicalNamingStrategy它会把驼峰命名的字段自动转成下划线风格实体里写 createTime生成的表字段是 create_time。这个策略的规则是连续小写字母保持原样遇到大写字母时在前面加下划线并将大写转小写。userName 会变成 user_namemyURL 会变成 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: trueSQL 会按缩进分多行打印可读性大幅提升。一个典型输出会长这样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 之间唯一可靠的沟通方式。希望这篇从最小例子到参数细节的拆解能让你后续遇到问题时先想到去验证而不是靠猜。希望帮到你。本文还有配套的精品资源点击获取
返回列表