Spring Boot+Hibernate+MySQL最小闭环与排错指南

发布时间:2026/10/31 22:06:11
Spring Boot+Hibernate+MySQL最小闭环与排错指南 简介这份 Spring Boot 学习项目面向刚接触 Java Web 与持久层框架的开发者围绕 Spring Boot 整合 Hibernate 连接 MySQL 展开重点演示实体映射、简单插入与查询等基础流程。压缩包共 155 个文件约 34.25MB以 jar 依赖包为主体同时包含 Java 源码与编译类、properties 配置、JSP 页面、XML 描述以及一份 SQL 数据库脚本并带有版本控制相关元数据便于直接导入 IDE 对照源码理解项目结构。已有 1829 人学习或下载。项目把依赖与建表脚本一并收拢适合用作课程练习、毕业设计原型或框架入门模板读者可从中了解 Spring Boot 与 Hibernate 的配置衔接、实体与表字段对应关系以及基础增查逻辑的组织方式。源码规模不大配置项集中适合按实体、映射、数据访问与页面展示几部分拆解学习也便于在此基础上改造表结构与查询条件借助现成脚本快速复现实验环境。1. 三件套跑通的最小闭环springboothibernatemysql 例子到底该包含什么有人把 springboot hibernate mysql 这个组合的原理想得很透但一上来就卡在启动日志里代码一行没改换台机器就跑不起来。问题往往不在注解写得好不好而在依赖坐标、连接串参数、建表语句这三处没有形成闭环。一个合格的“简单例子”至少要能证明四件事容器能起来、连接池能拿到连接、实体能映射到真实表、接口能完成一次写入并查回来。这四步缺任何一步后面加业务都是空中楼阁。这篇面向三类人刚接触 JPA 想搞清Entity与表之间到底怎么对应的新手手上有个老项目要从 JDBC 迁到 Hibernate 的从业者以及只想找一份能直接跑通、不用改十处的参照骨架的人。下面按依赖配置、实体映射、接口验证、故障排查、日志调试的顺序推进每一步都给出可直接抄的配置和能复现的命令。2. 依赖与连接配置pom 三个坐标和 yml 里的九个参数动手之前先明确一件事这份骨架的价值不在于代码多而在于每一行配置都有理由。新手最常见的做法是从别处拷一份 pom 和 yml然后逐个试错把报错消掉。这种做法在本地能跑一旦换环境就全部失效因为不知道哪个参数在起作用也就不知道哪个参数可以删。2.1 三个坐标的分工容器、ORM、驱动一个能跑的 springboot hibernate mysql 例子核心依赖只有三个其余都是可选的辅助。分工必须分清否则很容易陷入版本冲突的坑。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId !-- 示例版本实际请换成本地仓库里已经下载过的 3.x不要空着 -- version3.2.5/version /parent dependencies !-- 1. Web 层内嵌 Tomcat Jackson负责把对象序列化成 JSON -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 2. ORM 层传递引入 hibernate-core 与 spring-data-jpa -- !-- 重点不要再单独声明 hibernate-core否则版本会和 Boot 管的那个打架 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- 3. 驱动8.x 之后坐标从 mysql:mysql-connector-java 换成了 com.mysql:mysql-connector-j -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies参数说明上spring-boot-starter-parent决定了整条依赖链的版本只要它定了JPA、Hibernate、Jackson 的版本都不用写。spring-boot-starter-data-jpa是传递依赖它会把hibernate-core、spring-orm、spring-data-jpa一起拉进来所以手动再加一次hibernate-core是这个组合里最容易犯的错Maven 会按“最近优先”选一个版本结果可能是 Hibernate 6 的 jar 配上 Boot 2 的自动配置类启动时报NoSuchMethodError或者ClassNotFoundException而且堆栈指向框架内部看不出是自己加的依赖惹的祸。驱动的作用域必须是runtime。编译期代码里不该出现com.mysql.cj.jdbc.Driver这个类名因为 Spring Boot 会根据 URL 自动推断驱动写成runtime还有个好处换数据库时只改配置不改代码。如果非要在 yml 里显式写driver-class-name也可以但要写com.mysql.cj.jdbc.Driver老教程里的com.mysql.jdbc.Driver在 8.x 驱动里已经废弃启动时会打一行警告不影响运行但会误导排查。判断依赖是否干净用一条命令就够# 只看 hibernate 和 mysql 驱动最终解析到哪个版本确认没有被手动依赖顶掉 mvn -q dependency:tree -Dincludesorg.hibernate.orm:*,com.mysql:*输出里hibernate-core应该只有一行mysql-connector-j也应该只有一行。如果出现omitted for conflict或者同一坐标出现两个版本说明有人在 pom 里手动加过版本号回到上一步删掉即可。我一般会在给别人的骨架里保留这条命令的注释因为排查依赖冲突比排查逻辑错误省时间得多。2.2 application.yml 逐行连接串里每个参数为什么在这配置文件是这套组合的出问题最集中的地方。下面这份 yml 是能跑通的最小集合每个参数后面都有具体理由。server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltruerewriteBatchedStatementstrue username: demo password: demo123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1700000 jpa: database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: update show-sql: true open-in-view: false properties: hibernate: format_sql: true jdbc: batch_size: 30 logging: level: org.hibernate.SQL: debug参数作用不写会怎样useUnicodecharacterEncoding指定客户端字符集中文写入变成???且不报错serverTimezone告诉驱动服务端时区8.x 驱动直接抛时区无法识别异常useSSL关闭不必要的 TLS 握手本地连接多一次握手开销日志有警告allowPublicKeyRetrieval允许取服务端公钥8.x 默认认证插件下报公钥检索不允许maximum-pool-size连接池上限默认 10够用但要看数据库max_connectionsconnection-timeout拿连接的等待时间默认 30 秒出问题时整个请求挂死ddl-auto是否自动维护表结构生产误开update会悄悄改动字段open-in-view是否把 Session 延到视图渲染开着容易掩盖懒加载问题并在连接归还上出错batch_size批量写入时攒多少条发一次循环 insert 变成 N 次网络往返这里需要单独说明open-in-view。它的默认值是 true作用是让 Hibernate 的 Session 一直存活到响应序列化完成好处是 Controller 层拿到懒加载集合不会立刻报错坏处是数据库连接在整段序列化期间都被占着高并发下连接池很快耗尽而且懒加载异常被推迟到难以定位的位置才暴露。这份骨架把它设为 false意味着所有需要的数据必须在 Service 的事务内取完这样延迟加载问题会在开发期暴露而不是在线上暴露。ddl-auto的取值需要单独列清楚因为它是新手最容易误用的一个开关。取值行为适用场景none不做任何结构操作生产环境配合手工迁移脚本validate只校验不匹配则启动失败预发、联调用来抓实体与表不一致update缺表建表、缺列加列不删不改本地开发、演示例子create每次启动先删表再建表单元测试、每次都要干净数据create-drop启动建、关闭删内存库的集成测试一个常见做法是本地用update快速迭代提交前改成validate跑一次确认实体和建表脚本完全对齐再进测试环境。这样既享受了自动建表的便利也不会把“靠 Hibernate 猜表结构”的习惯带进生产。2.3 建库建表 SQL 与账号授权即使开着update我也建议把建表语句显式写出来。原因很实际Hibernate 自动生成的表字段类型、索引名、字符集都由方言决定换一个版本可能就不一样而显式脚本是可评审、可版本管理的。-- 字符集一定用 utf8mb4MySQL 的 utf8 是 3 字节存不下四字节字符 CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE demo_db; CREATE TABLE IF NOT EXISTS t_product ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, created_at DATETIME NULL, PRIMARY KEY (id), KEY idx_product_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 应用账号只给这个库的增删改查和建表权限不要图省事给 ALL CREATE USER IF NOT EXISTS demo127.0.0.1 IDENTIFIED BY demo123; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON demo_db.* TO demo127.0.0.1; FLUSH PRIVILEGES;几个参数值得说明DECIMAL(10,2)而不是DOUBLE金额用浮点会在累加时出现精度偏差这个在商品价格场景里是硬性要求DATETIME和TIMESTAMP的区别在于前者不做时区转换、后者会配合serverTimezone时如果不一致查出来的时间会整体偏移八小时属于典型的“看着对但就是差一点”的问题ENGINEInnoDB是事务和行锁的前提MyISAM不支持事务Transactional在上面完全不起作用。账号的主机部分写127.0.0.1而不是%是为了让连接来源固定。如果写成%MySQL 会按“最具体匹配”选账号本机上同时存在demolocalhost和demo%时实际用的可能不是你以为的那个密码对不上就报Access denied。这类问题在排查时最费时间因为配置看上去完全正确。3. 实体映射与仓储层把 t_product 表变成可查询的对象配置通了之后接下来是把表变成 Java 对象。这一层的核心不是注解多而是知道每个注解在生成 SQL 时到底起了什么作用。搞不清这一点后面所有查询问题都会变成猜测。3.1 Entity 到表结构字段类型与主键策略package com.example.demo.domain; import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; Entity Table(name t_product, indexes Index(name idx_product_name, columnList name)) public class Product { // MySQL 自增主键必须用 IDENTITY不能用 AUTO Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name name, nullable false, length 64) private String name; Column(name price, nullable false, precision 10, scale 2) private BigDecimal price; Column(name stock, nullable false) private Integer stock 0; // updatable false插入时写入后续 update 语句不带这一列 Column(name created_at, updatable false) private LocalDateTime createdAt; PrePersist void onCreate() { this.createdAt LocalDateTime.now(); if (this.stock null) { this.stock 0; } } // getter / setter 必须补齐Jackson 序列化和 JPA 属性访问都依赖它们 }jakarta.persistence这个包名是关键。Spring Boot 3 起Java EE 迁移到 Jakarta EE包名从javax.persistence变成了jakarta.persistence。网上大量教程还是javax直接拷过来会报package javax.persistence does not exist。如果项目确实还停在 Boot 2.x那就得整篇换成javax两者不能混用混用的表现是编译通过但启动时实体扫描不到Entity形同虚设。主键策略这一项单独强调MySQL 场景下必须用GenerationType.IDENTITY。用AUTO时Hibernate 6 会默认选择基于序列的策略而在 MySQL 上没有原生序列它会退化成一张额外的序列表来模拟结果就是每次插入多一条select ... for update和一次update压测时这部分开销相当可观。用TABLE同理。用IDENTITY则是真正依赖AUTO_INCREMENT插入后由驱动回填主键开销最小。索引的写法也值得注意。Index只有在ddl-auto为create或update且表由 Hibernate 创建时才生效如果你的表是手工 SQL 建的这里的注解只是文档性质真正的索引得写进建表语句。我一般两处都写保持它们一致因为读实体的人不一定会去看 SQL 文件。命名策略容易被忽略。如果不写TableBoot 默认的CamelCaseToUnderscoresNamingStrategy会把Product映射成product把createdAt映射成created_at。这个策略对纯小写表名友好但一旦表名带前缀比如t_product就必须显式写Table。反过来如果数据库里是tProduct这种驼峰表名就得在 yml 里改物理命名策略否则怎么都对不上。3.2 Repository方法名派生、JPQL、批量更新三种写法package com.example.demo.repository; import com.example.demo.domain.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.math.BigDecimal; import java.util.List; public interface ProductRepository extends JpaRepositoryProduct, Long { // 写法一方法名派生findBy 属性 关键字 OrderBy // 生成where name like ? order by id desc ListProduct findByNameContainingOrderByIdDesc(String keyword); // 写法二JPQL写实体名和属性名不是表名和列名 Query(select p from Product p where p.price between :min and :max order by p.price) ListProduct findByPriceRange(Param(min) BigDecimal min, Param(max) BigDecimal max); // 写法三分页返回值是 Page 时会自动执行一条 count 查询 PageProduct findByStockGreaterThan(Integer stock, Pageable pageable); // 写法四批量更新必须加 Modifying且调用方要有事务 // 返回值是受影响行数可以用它做乐观并发判断 Modifying(clearAutomatically true) Query(update Product p set p.stock p.stock - :n where p.id :id and p.stock :n) int deductStock(Param(id) Long id, Param(n) Integer n); }四种写法的取舍标准很明确。方法名派生适合条件少于三个的固定查询超过三个方法名会长到没人愿意读而且And连接的语义是“全部满足”做不了or的分组这时候必须换 JPQL。JPQL 的优势是面向对象、可读、能跨数据库缺点是拼不了动态条件。真正需要按前端传参动态拼条件时用Specification或者Criteria这是另一套东西简单例子里不必引入。Modifying这条要重点说。不加它执行 update 语句会直接抛异常因为 Spring Data 默认认为所有Query都是查询。加了之后还必须有事务否则会报TransactionRequiredException因为更新操作需要显式提交。clearAutomatically true的作用是更新后清空一级缓存否则同一个事务里先前查出来的实体还在缓存中再查会拿到旧值表现为“我明明改了但查出来还是老数据”。批量更新还有一个隐含限制它绕过 Hibernate 的实体生命周期PreUpdate、LastModifiedDate这类回调都不会触发审计字段不会自动更新。这一点在给已有项目加审计逻辑时经常出问题写的时候就要判断清楚如果这张表依赖回调维护字段就别用批量更新老老实实findById再set。3.3 Service 与事务边界画在哪一层package com.example.demo.service; import com.example.demo.domain.Product; import com.example.demo.repository.ProductRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; Service public class ProductService { private final ProductRepository repo; // 构造器注入不要用字段注入单测时能直接 new public ProductService(ProductRepository repo) { this.repo repo; } // 只读事务Hibernate 会跳过脏检查读多写少的场景有明显收益 Transactional(readOnly true) public PageProduct page(int page, int size) { return repo.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, id))); } Transactional(readOnly true) public OptionalProduct findById(Long id) { return repo.findById(id); } // rollbackFor Exception.class默认只对 RuntimeException 回滚 Transactional(rollbackFor Exception.class) public Product create(Product p) { return repo.save(p); } Transactional(rollbackFor Exception.class) public void deduct(Long id, Integer n) { int affected repo.deductStock(id, n); if (affected 0) { // 库存不足或商品不存在抛异常触发回滚 throw new IllegalStateException(库存不足或商品不存在: id); } } }事务边界放在 Service 而不是 Repository理由是一个业务动作可能涉及多次仓储调用只有放在 Service 才能保证它们在同一事务内。放在 Repository 上每次调用都是独立事务中途失败前面的写入不会回滚。rollbackFor Exception.class这一条是血泪经验。Transactional默认只在遇到RuntimeException和Error时回滚受检异常不回滚。如果业务代码里抛了自定义的受检异常事务会照常提交数据半截写入而且日志上看不出任何异常只有对数据的时候才发现对不上。还有一个 AOP 层面的问题同类内部方法互相调用时事务注解不生效。比如create里直接调this.deduct(...)deduct上的事务注解会被绕过因为它没经过 Spring 生成的代理对象。解决办法是把被调方法挪到另一个 Bean或者注入自身代理。4. 接口闭环实测4 个 CRUD 接口与 curl 验证清单实体和仓储都有了最后一步是把链路打通并验证。这一步不要靠“看着像对”要用具体的请求和具体的返回体去对。一个能跑通的例子验证过程本身就应该能被别人照着重放。4.1 Controller 四个接口的最小实现package com.example.demo.web; import com.example.demo.domain.Product; import com.example.demo.service.ProductService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/products) public class ProductController { private final ProductService service; public ProductController(ProductService service) { this.service service; } // 分页查询page 从 0 开始这是最容易和前端对不上的地方 GetMapping public PageProduct list(RequestParam(defaultValue 0) int page, RequestParam(defaultValue 10) int size) { return service.page(page, size); } GetMapping(/{id}) public ResponseEntityProduct get(PathVariable Long id) { return service.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } PostMapping public Product create(RequestBody Product p) { return service.create(p); } PutMapping(/{id}) public Product update(PathVariable Long id, RequestBody Product p) { return service.update(id, p); } DeleteMapping(/{id}) public ResponseEntityVoid delete(PathVariable Long id) { service.delete(id); return ResponseEntity.noContent().build(); } }RestController等价于Controller加ResponseBody返回值直接由 Jackson 序列化成 JSON不走视图解析。ResponseEntity用来控制状态码查不到返回 404删除成功返回 204这两个语义比一律 200 更利于前端判断。page的默认值写 0 是因为 Spring Data 的页码从 0 开始而前端传参习惯从 1 开始。这个差异必须在接口层说清楚否则第一页的数据永远被跳过。我一般会在文档里显式写明或者干脆在 Controller 里接收 1 基页码再减一避免每次联调都有人问“为什么少了一条”。RequestBody Product p直接接收实体对象在简单例子里可以接受但在正式项目里不推荐。实体会带上id、createdAt这类由服务端维护的字段客户端传什么就写什么容易造成误覆盖。规范做法是定义一个ProductCreateRequest只包含可写字段这里为了保持例子简洁暂时省略但从一开始就应该知道这是个待改进点。4.2 curl 逐条验证与返回体检查# 1. 新增POST 一条数据观察返回体里 id 是否被回填 curl -s -X POST http://127.0.0.1:8080/api/products \ -H Content-Type: application/json \ -d {name:测试商品A,price:19.90,stock:100} # 2. 单查确认 id、created_at 都正确写入 curl -s http://127.0.0.1:8080/api/products/1 # 3. 分页确认 totalElements 和 content 的长度 curl -s http://127.0.0.1:8080/api/products?page0size5 # 4. 更新全量覆盖注意没传的字段会被置为 null curl -s -X PUT http://127.0.0.1:8080/api/products/1 \ -H Content-Type: application/json \ -d {name:测试商品A-改,price:29.90,stock:80} # 5. 删除应返回 204无响应体 curl -s -o /dev/null -w %{http_code}\n -X DELETE http://127.0.0.1:8080/api/products/1验证的重点不是“有没有返回”而是几个具体字段。新增后返回体里的id必须有值且与数据库里一致这是IDENTITY策略生效的证据createdAt必须有值这是PrePersist生效的证据分页返回里的totalElements、totalPages、number、size四个字段要能对上number应为 0 而不是 1。更新这一步要特别注意“全量覆盖”语义。PUT用的是saveHibernate 对游离态对象执行的是 merge没传的字段会变成 null 写入数据库。如果只想改价格应该用PATCH加局部更新或者在 Service 里先查出来再逐个字段赋值。这个坑在实际项目里非常常见表现为“改了个名字库存清零了”。4.3 分页、排序与条件查询的落地细节分页接口在真实使用中通常还要带排序和条件。排序参数可以直接通过Sort.by构造也可以交给 Spring 从请求参数解析# 按价格升序、id 降序多字段排序直接用逗号分隔 curl -s http://127.0.0.1:8080/api/products?page0size10sortprice,ascsortid,desc要让这种写法生效Controller 的参数需要改成接收PageableGetMapping public PageProduct list(Pageable pageable) { return repo.findAll(pageable); }Spring 会自动把page、size、sort三个参数装配成Pageable。默认size是 20sort不传则按主键。生产环境一定要给size加上限否则前端传个size100000就能把内存打满做法是配置spring.data.web.pageable.max-page-size或者在 Controller 里手动截断。带条件的查询则要考虑 count 查询的开销。PageT会自动生成一条select count(*)条件复杂时这条 count 可能比主查询还慢。解决办法是给Query显式指定countQuery或者在数据量大的场景改用SliceT只查下一条是否存在不做总数统计。这个取舍在简单例子里体现不出来但一旦表过百万行就会立刻显现。5. 避坑排查启动、连接、映射三类翻车现场各拆一遍这套组合的报错有个共同特点堆栈指向框架内部真正的错误原因被埋在几十行Caused by后面。下面按出现阶段拆开每条都给出可直接对照的处置方式。5.1 启动阶段数据源与依赖版本现象一启动直接失败日志里是Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be configured。原因三种可能。一是 yml 里spring.datasource的缩进错了YAML 对空格敏感用 Tab 或者多缩进一层都会让这段配置变成另一个节点的子属性二是spring-boot-starter-data-jpa引入了但驱动没引入Spring 找不到可用的连接实现三是项目里同时存在application.properties和application.ymlproperties 优先级更高且内容不全覆盖掉了 yml 里的配置。解决先跑mvn dependency:tree -Dincludescom.mysql:*确认驱动在不在。再检查target/classes下有几个配置文件有多个就删掉不用的那个。缩进问题用 IDE 的 YAML 插件看层级spring下的datasource必须是四个空格缩进。现象二编译通过启动时报java.lang.NoClassDefFoundError: javax/persistence/Entity或NoSuchMethodError: org.hibernate...。原因包名或版本错配。Boot 3 用jakarta.persistence代码里写javax就编译不过但如果是旧代码编译产物残留或者 pom 里手动加了hibernate-core的低版本就会在运行时才炸。解决全局搜索javax.persistence替换成jakarta.persistencemvn dependency:tree确认hibernate-core只有一行执行mvn clean清掉旧的编译产物再启动很多时候“改了没生效”就是 target 目录没清。5.2 连接阶段认证、时区与网络现象三java.sql.SQLException: Public Key Retrieval is not allowed。原因MySQL 8 默认认证插件是caching_sha2_password客户端首次连接需要向服务端索取公钥来完成密码加密驱动默认禁止这个行为。解决在连接串加allowPublicKeyRetrievaltrue。如果同时开了useSSLtrue这个参数就不必要了因为 TLS 通道已经能保证安全。本地开发用前者更方便线上则应该配好证书走后者。现象四The server time zone value 中国标准时间 is unrecognized or represents more than one time zone。原因驱动拿到的服务端时区是一个本地化名称无法映射成ZoneId。Windows 上的 MySQL 尤其容易出这个问题。解决连接串里显式写serverTimezoneAsia/Shanghai。注意这个参数只影响驱动对时间的解释不改变数据库存储的值。如果同时用TIMESTAMP字段还要确认 MySQL 服务端的time_zone变量两边不一致时读写会各偏一次这是“存进去是九点、查出来是十七点”的根因。现象五Access denied for user demolocalhost但密码明明是对的。原因MySQL 的账号是“用户名 主机”二元组。demo127.0.0.1和demolocalhost是两个不同账号驱动解析localhost时可能走 socket 也可能走 TCP匹配到的账号不同。解决select user, host from mysql.user where userdemo;看清楚有哪几条记录。应用侧统一用127.0.0.1而不是localhost并在建账号时按同样的主机写授权两边对齐就不会有歧义。5.3 映射与写入阶段表名、主键与事务现象六Table demo_db.t_product doesnt exist但表确实建了。原因ddl-auto设成了none或validate而实体映射出来的表名和实际表名不一致。常见差异是下划线命名策略没生效、Table拼写错误、或者 MySQL 在 Linux 上默认lower_case_table_names0区分大小写本地 Windows 上不区分所以看不出来。解决打开show-sql看启动时校验语句里用的表名到底是什么与show tables的结果逐个字符比对。跨平台部署时统一用小写加下划线命名是最省事的约定。现象七批量更新接口返回成功但数据库里数据没变。原因三种可能。Modifying漏了或者加了但调用方没有事务方法上加了Transactional但被同类内部调用绕过代理clearAutomatically没开读到的是缓存里的旧值。解决先在show-sql输出里确认 update 语句到底有没有发出。发出了但数据没变查事务提交日志没发出查Modifying和代理调用链。判断是否走了代理有个简单办法在方法里打一行日志如果事务注解生效日志会在TransactionInterceptor之后输出。6. 把 Hibernate 的 SQL 日志变成调试黑匣子三个开关与一张观察表排查到最后你会发现绝大多数“玄学问题”都能从实际发出的 SQL 里看出答案。问题在于默认配置下你看不到它或者看到了也是一堆挤在一起的单行文本。我习惯在骨架里就把三个开关配好出问题时不用改代码改配置重启即可。spring: jpa: show-sql: true # 由 Spring 打印格式固定信息量最小 properties: hibernate: format_sql: true # 把 SQL 格式化成多行参数一目了然 generate_statistics: true # 输出每次请求的查询次数、耗时、缓存命中 logging: level: org.hibernate.SQL: debug # 打印 SQL 语句本身 org.hibernate.orm.jdbc.bind: trace # Boot 3.x打印绑定参数 # Boot 2.x 用这行替换上一行org.hibernate.type.descriptor.sql.BasicBinder: trace开关能看到什么典型用途show-sql单行 SQL快速确认语句生成了没format_sql多行带缩进的 SQL看 where 条件、join 顺序org.hibernate.SQL与show-sql类似但走日志框架配合级别控制生产可临时打开绑定参数trace每个?实际填入的值排查“条件明明传了却查不到”generate_statistics单次请求的 SQL 条数抓 N1 查询最后一项最有用。开启后控制台会输出类似Session Metrics {nanoTime, sessionOpenCount, ... , statementCount: 5, ...}的汇总行重点看statementCount和prepareStatementCount。一个列表接口返回 10 条数据如果这里显示 11 次查询那就是典型的 N1说明关联对象是一个一个懒加载出来的解决办法是改用join fetch或者EntityGraph一次性抓取。有一点必须提醒generate_statistics有实实在在的性能开销它会在每个 JDBC 调用前后记录时间戳压测时会拉低吞吐。所以它的定位是“本地和预发环境的诊断工具”不能常驻生产。我一般的做法是把它写进application-dev.yml生产 profile 里压根不出现这个键。从那次为了找一个“列表页偶发超时”的问题、盯着 300 多行挤成一坨的 SQL 翻了两个小时之后我每次新建这类骨架都强制先把上面这套日志配置加上再写第一行业务代码。看起来是多花了五分钟实际省下的是几个小时的黑盒猜测。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询