ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot 3 + Hibernate 6 + MySQL 最小CRUD实践:从版本选型到避坑

Spring Boot 3 + Hibernate 6 + MySQL 最小CRUD实践:从版本选型到避坑 简介面向Spring Boot初学者这份资源演示了如何以Hibernate作为ORM框架连接MySQL并完成简单的数据插入与查询操作适合正在学习Spring Boot整合数据访问层、或希望快速搭建一个可运行示例的开发者参考。压缩包共155个文件整体约34.25MB其中包含64个jar依赖、6个java源文件、2个jsp页面、2个xml配置、1个数据库建表脚本sql同时附有工程配置文件与SVN元数据结构完整清晰。目前已有1826人学习下载热度表现稳定。项目提前集成了所需jar包和数据库初始化脚本省去手动下载依赖与构造数据的步骤只需创建数据库并执行sql脚本即可运行通过这个简单实例可以直观理解Spring Boot启动流程、Hibernate实体映射以及基础CRUD写法的配合方式对后续整合复杂业务场景也有不错的参考价值。1. 搜这个标题的人最需要的是一份能直接跑起来的清单Hibernate 还有人用吗只要你还用 Spring Boot答案就是它一直在。springboothibernatemysql 这个组合常被当成“老技术”但 Spring Boot 3.x 默认的 ORM 组件仍然是 Hibernate 6MySQL 驱动坐标和持久化包名却都换了网上大量教程还在讲 Spring Boot 2.x 的写法。结果就是代码明明按教程写的一编译就报 javax 包不存在。这篇笔记围绕一个最小可运行例子展开把版本选型、配置参数、实体映射、CRUD 接口和排错顺序一次说清。适合三类人课程作业需要交 demo 的学生、接手老项目的开发者以及只想在本地快速验证 Spring Boot MySQL 打通的人。读完你能得到一个可查可增可删的接口而不是一个跑不起来的半成品。2. 版本选型决定成败Spring Boot 2.7 还是 3.xHibernate 5 还是 62.1 Spring Boot 版本决定 Hibernate 的脾气这个组合的第一个坑不在代码在版本。Spring Boot 自带 Hibernate 版本管理你单独指定 hibernate-core 的版本反而容易和 Boot 打架。我一般先定 Spring Boot 版本再谈其他。Spring Boot 版本默认 Hibernate 版本持久化 API 包名推荐 Java 版本2.7.x5.6.xjavax.persistence8 或 113.0.x6.2.xjakarta.persistence17 起3.2.x / 3.3.x6.4.x / 6.5.xjakarta.persistence17 起新手最常见的翻车点就在这张表上搜到一篇 Spring Boot 2.7 的教程照着写import javax.persistence.*;然后自己建了个 Spring Boot 3.2 项目一编译就报package javax.persistence does not exist。这不是你不会写代码是 Boot 3.x 里持久化 API 整体迁移到了 Jakarta EE 9 的命名空间javax.persistence改成了jakarta.persistence。Hibernate 6 的实体注解、EntityManager接口、事务注解全部跟着变了。所以动手前先问自己两个问题第一你本机 Java 是多少Java 8 就别硬上 Boot 3.x老老实实用 2.7.18这是 2.x 最后一个版本稳定且能跑 Java 8。第二你抄的教程是哪个年代的教程里出现javax就配 2.7出现jakarta就配 3.x。做这个例子本身我倾向直接选 Spring Boot 3.2.x 或更新的 3.xHibernate 6 的方言自动识别比老版本省心后面会讲到。2.2 starter 依赖怎么选让 JPA starter 帮你带 Hibernate先给一份最小 pom.xml这个文件能直接抄。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这里有两个容易被旧教程带偏的地方。第一不要手动加hibernate-core。spring-boot-starter-data-jpa会自动带 Hibernate 核心包手动加一个特定版本反而可能破坏 Boot 的版本管理出现奇怪的NoSuchMethodError。第二MySQL 驱动坐标变了。Spring Boot 2.x 时代写的是mysql:mysql-connector-javaBoot 3.x 里官方把坐标改成了com.mysql:mysql-connector-j。如果你搜到老教程把坐标原样抄进 Boot 3 项目依赖会解析失败控制台会提示找不到这个 artifact。starter-web不是必须的但既然是“简单例子”最后要有一个 HTTP 接口去验证 CRUD所以一起加上。如果不想要 Web 层可以去掉它用ApplicationRunner做验证但对大多数照着例子学的人来说接口更直观。2.3 别手动配方言Hibernate 6 会自己认老教程里几乎都有这么一行配置spring.jpa.properties.hibernate.dialectorg.hibernate.dialect.MySQL8Dialect这是 Hibernate 5 时代的习惯。Hibernate 6 开始方言会自动根据 JDBC 连接信息识别MySQL 8 连上去就会选 MySQLDialect不再需要手写。你硬写一个老方言类进去反而可能遇到类不存在或已废弃的提示。所以这个例子里我对 dialect 的态度是不配。这不是偷懒是 Hibernate 6 确实把这一步自动化了。如果你确实要在日志里确认自己用的是哪个方言启动时留意日志里Using dialect:这一行即可。从这里能看到 Hibernate 自己选了什么也方便核对你抄来的配置有没有画蛇添足。版本和依赖这部分理顺了后面的配置才有的聊。3. 把配置写对从 application.yml 到实体映射的四个关键点3.1 先建库再写 application.yml先在你的 MySQL 里建一个测试库。用命令行、Navicat 都行SQL 是这样的CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;字符集用 utf8mb4别用老旧的 utf8不然存 emoji 或生僻字时 MySQL 会报Incorrect string value。如果你是在 Windows 上装 MySQL 8.0安装时字符集选项就选 utf8mb4比装完再改配置省事。MySQL 5.7 里这句 SQL 也能用兼容性没问题。然后是 Spring Boot 配置文件我放在src/main/resources/application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false properties: hibernate: format_sql: trueURL 里的参数逐个说清楚useUnicodetruecharacterEncodingutf8保证中文以 UTF-8 进出serverTimezoneAsia/Shanghai解决 MySQL 8.0 的时区报错不写会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognizeduseSSLfalse关掉 SSL 握手本地开发没必要加密allowPublicKeyRetrievaltrue是配合 MySQL 8 默认的caching_sha2_password认证用的不加会在连接时报Public Key Retrieval is not allowed。ddl-auto: update是让 Hibernate 启动时按实体类自动建表或补列适合起步阶段。show-sql: true把 Hibernate 生成的 SQL 打到控制台。open-in-view: false是为了避免 Spring Boot 默认开启 OSIV 导致的连接占用后面避坑章节会专门讲它。3.2 实体类映射表名、列名和关键字一个都不能错建一个User实体package com.example.demo.entity; import jakarta.persistence.*; import java.time.LocalDateTime; Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_name, length 50, nullable false) private String userName; Column(name email, length 100) private String email; Column(name create_time) private LocalDateTime createTime; public User() { } // getter / setter 省略实际代码里要补全 }三个细节值得圈出来。第一Table(name t_user)里的表名故意加了个t_前缀。MySQL 8 里user、order、group都是保留字或半保留字表名直接叫user的话Hibernate 自动建表可能成功但你手工执行SELECT * FROM user时 MySQL 会要求给表名加反引号。用t_user一劳永逸。第二Column(name user_name)我写了显式列名。Spring Boot 默认的物理命名策略会把实体属性userName自动转成列名user_name这本身没错。但如果你手工建表时列名写的是userNameHibernate 却按user_name去查就会报Unknown column。与其赌默认策略不如把name写死新手少一个变量。第三GenerationType.IDENTITY对应 MySQL 的自增主键。Hibernate 6 里用AUTO在某些 MySQL 版本下会选到TABLE策略产生额外的锁表操作本地例子无所谓但IDENTITY更贴合 MySQL 的习惯。实体类还必须有一个无参构造器Hibernate 反射创建对象时要用。如果你写了带参构造器却忘了补无参的启动时 Hibernate 会直接报No default constructor for entity。3.3 ddl-autoupdate 和手工建表只能二选一ddl-auto: update方便但有脾气它只会加表、加列不会删列也不会改已有列的类型。你把实体里String email改成Integer emailHibernate 不会去改 MySQL 里已存在的email列类型启动时甚至可能不报错等查询时才出奇怪的数据转换异常。如果你更想自己掌控表结构可以手工建表再把ddl-auto改成noneCREATE TABLE IF NOT EXISTS t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL, email VARCHAR(100), create_time DATETIME ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;两种方式不冲突但别混着来。用ddl-auto: update就别同时手工改表否则两边总会有一个先跑。我一般建议新手先开update跑通流程跑通了再关掉改成手写 SQL 管理表结构。这个习惯能帮你提前避开生产环境禁用update时的措手不及。4. 跑通最小 CRUD 例子Repository、事务与接口一次到位4.1 Repository先写接口别急着写实现Spring Data JPA 的核心用法是你只写接口实现由框架在运行时生成。先定义一个UserRepositorypackage com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface UserRepository extends JpaRepositoryUser, Long { User findByUserName(String userName); Query(select u from User u order by u.createTime desc) ListUser findLatestUsers(); Query(select u from User u where u.email :email) ListUser findByEmail(Param(email) String email); }JpaRepositoryUser, Long里第一个泛型是实体类第二个是主键类型。这个接口自带一批方法findById、findAll、save、deleteById、count不用自己写实现。findByUserName是 Spring Data 的命名查询它会按属性名自动生成WHERE user_name ?前提是实体里有userName这个属性方法名的大小写不能错。Query里写的是 HQL不是 SQL操作对象是实体名User和属性名createTime不是表名和列名。order by u.createTime desc就是想让结果按创建时间倒序对应那条热搜词“mysql 排序”实际上排序这活儿在 HQL 里写比拼 SQL 更安全Hibernate 会帮你翻译成正确的 MySQL 方言。4.2 Service 与事务为什么中间要夹一层Repository 可以直接在 Controller 里用但例子再简单我也建议加 Service 层因为事务边界应该放在 Service 上。写一个UserServicepackage 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; import java.time.LocalDateTime; import java.util.List; Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository userRepository; } Transactional public User createUser(String userName, String email) { User user new User(); user.setUserName(userName); user.setEmail(email); user.setCreateTime(LocalDateTime.now()); return userRepository.save(user); } Transactional(readOnly true) public ListUser listUsers() { return userRepository.findAll(); } }构造器注入是 Spring 官方推荐的方式final字段配合构造器比Autowired字段注入更好测、更好排查空指针。Transactional负责开启数据库事务createUser里的save会在方法返回前提交listUsers标注readOnly true只是给事务管理器一个优化提示不会真的把查询放到只读事务里但语义更准确。注意 Spring Boot 默认使用 CGLIB 代理也就是基于子类的代理所以Transactional写在实现类上也有效不要求接口。但这也带来一个经典坑同一个类里this.createUser()调用另一个Transactional方法事务注解会失效。事务生效靠的是代理对象this调用绕过了代理。遇到这种需求就拆分 Bean别自己调自己。4.3 Controller 与启动验证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 create(RequestParam String userName, RequestParam String email) { return userService.createUser(userName, email); } GetMapping public ListUser list() { return userService.listUsers(); } }到这里代码就齐了。启动方式有两种命令行执行mvn spring-boot:run或者在 IDEA 里直接运行启动类的main方法。IDEA 里如果默认端口被占可以在 Run Configuration 的 Program arguments 里加--server.port8081不用改配置文件。跑起来之后另开一个终端验证接口curl -X POST http://localhost:8080/api/users?userNametestemailtestexample.com curl http://localhost:8080/api/users第一条返回新增的 User JSON第二条返回列表。如果 POST 时输入的参数没带RequestBody用RequestParam就要求 URL 里带参数。想用 JSON 提交也可以把 Controller 参数换成RequestBody User user但那样createTime也需要前端传例子会多一个 DTO所以这里就不展开。验证通过后去 MySQL 里SELECT * FROM t_user能看到 Hibernate 自动建的表和插入的行。5. 常见问题与排查照着例子还跑不通的 5 类情况5.1 编译直接报javax.persistence不存在现象代码和网上教程一模一样Maven 编译时报package javax.persistence does not exist。原因你的 Spring Boot 是 3.xHibernate 6 把持久化 API 迁移到了jakarta.persistence老教程基于 Boot 2.x 的javax命名空间。解决全局替换javax.persistence为jakarta.persistencejavax.transaction同理。顺带把hibernate-core的手动依赖删掉让spring-boot-starter-data-jpa统一管理版本。如果项目必须用 Java 8只能降级到 Spring Boot 2.7.18而不是硬扛 Boot 3。5.2 启动连库报Public Key Retrieval is not allowed或 SSL 连接错误现象应用启动时日志出现Public Key Retrieval is not allowed或者Communications link failure还有 MySQL 8 下常见的 SSL 连接报错。原因MySQL 8 默认认证插件是caching_sha2_passwordJDBC 驱动首次连接时需要请求服务器公钥而 URL 里既没允许公钥检索又没关 SSL。解决在 datasource URL 末尾拼上useSSLfalseallowPublicKeyRetrievaltrue两个参数要同时存在。如果连 5.7useSSLfalse可以去掉但留着也不影响。另外确认 MySQL 服务真的起来了Windows 服务管理器里 MySQL80 服务没启动的话报的会是连接超时而不是密码错误别在密码上浪费太多时间。5.3 查询报Unknown column user0_.create_time in field list现象表里明明有create_time列查询却报列不存在。原因大概率是你手工建表时列名写成了createTime驼峰形式而 Hibernate 的默认命名策略把实体属性createTime转成了create_time。两边对不上SQL 自然执行失败。解决以 Hibernate 要的为准要么把表列名改成下划线风格要么在实体Column里显式写name create_time。我推荐后者因为它同时把代码意图写清楚了换同事接手也不用靠猜。另外如果表名用了 MySQL 保留字执行create table user (...)时也会报语法错误这也是我坚持t_user前缀的原因。5.4 加了关联后接口一返回就抛 LazyInitializationException现象例子里只有一个实体是跑不通的但很多人会接着加OneToMany关联然后在 Controller 返回实体时抛LazyInitializationException: could not initialize proxy - no Session。原因关联集合默认懒加载Session 在 Service 事务结束时已经关闭Controller 里访问未加载的集合时Hibernate 拉不到 Session。Spring Boot 的open-in-view默认是 true它把 Session 延长到了 HTTP 请求结束所以很多人一开始不报错但控制台一直在刷Consider setting open-in-view to false的警告。解决把spring.jpa.open-in-view设为false然后在 Service 事务内把需要的关联数据查好Controller 返回 DTO不要直接吐实体。这样虽然多写一点代码但连接不会长时间占用接口响应也更可控。5.5 事务好像没生效方法抛了异常数据还是写进去了现象createUser里故意throw new RuntimeException()结果save的数据还是落库了。原因第一Transactional是走代理生效的如果这个方法是被同一个类的另一个方法this.xxx()调用的注解直接失效第二Spring 默认只对RuntimeException和Error回滚受检异常不会主动回滚。解决事务方法放到独立 Bean 里通过注入调用而不是this调用如果确实要让受检异常也触发回滚在注解上写Transactional(rollbackFor Exception.class)。验证事务最简单的方法是往方法里抛一个运行时异常看数据库有没有新增行这一步值得做一次能帮你确认事务真的在工作。6. 让 Hibernate 把 SQL 亮出来靠日志验证映射的最后一步6.1 打开 SQL 日志两行配置show-sql: true能看到的只是 Hibernate 打出来的 SQL 语句如果你想连预编译参数一起看再加一段日志配置logging: level: org.hibernate.SQL: DEBUG org.hibernate.type: TRACEorg.hibernate.SQL: DEBUG会把 SQL 完整打印出来org.hibernate.type: TRACE会输出绑定到?上的参数值。启动后你会看到类似insert into t_user (create_time, email, user_name) values (?, ?, ?)的语句以及后面的实际参数。这个习惯我一直保留着因为实体和数据库之间是黑匣子不把 SQL 亮出来你永远不知道 Hibernate 到底在查哪一列。6.2 用生成的 SQL 反查实体设计第一次启动时重点看 Hibernate 自动建的create table语句逐列和你的实体Column对一遍列名、类型对不对一目了然。请求接口时再看select和insert语句对照你预期的查询。比如findByUserName如果生成的 SQL 是where t_user.user_name ?说明命名策略按预期工作如果跑出where t_user.user_name1_ ?这种带序号的列名基本可以断定实体里字段写重了或命名策略混乱。我早期写实体不看日志结果 Hibernate 帮我建的表和预期差了一列花了半小时才找出来。后来养成习惯实体改完先启动看 SQL再跑接口最后确认数据。这个顺序看着笨却是最快抓出映射错误的手段。Hibernate 的映射问题大多数都能在 SQL 日志这层暴露出来比追堆栈快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表