简介:这是一份供Spring Boot初学者练习的完整项目示例,围绕Hibernate连接MySQL数据库展开,演示了基础的插入与查询操作,适合已经掌握Java语法、希望快速上手Spring Boot与ORM框架整合的开发者,也适合作为课程设计或自学参考。压缩包共155个文件,整体大小约34.25MB,主要内容包括64个依赖JAR包、6个Java源文件、6个编译后的class文件、2个JSP页面、3个properties配置文件、2个XML配置以及1个SQL建表脚本;JAR包使项目无需额外下载依赖,源代码展示核心业务逻辑,SQL脚本帮助快速初始化数据表,压缩包中还保留了SVN元数据文件,说明内容源自真实工程。项目中已包含全部依赖和初始化数据表的SQL脚本,调整数据库连接配置后即可直接启动,直观体验Hibernate的实体映射、构建SessionFactory、事务处理等关键环节。截至目前已有1824人学习/下载,对想要获得可直接运行调试的Spring Boot+Hibernate+MySQL入门项目来说是性价比很高的参考。整体结构清晰,配置与源码分离,便于逐文件对照学习,掌握后可以快速迁移到其他业务场景中。 不知道你有没有遇到过这种需求:领导丢过来一句话,"用 Spring Boot 搭个后台,能连 MySQL 存数据就行"。听起来简单,但实际上 Spring Boot 的持久层方案早就不是单选题,Hibernate 和 MyBatis 各有一拨拥趸,网上教程又各说各话,新手很容易卡在第一步的选择上。我最后选了 Hibernate,写了一个 Spring Boot + Hibernate + MySQL 的最简例子,从环境准备到接口跑通大概花了一个下午,中间踩了不少坑。这篇文章就把整个流程和排查过程记录下来,适合刚接触这三个组件、或者一直用 MyBatis 想快速切到 Hibernate 的读者。
1. 老生常谈的选型:Hibernate 凭什么能少写代码
1.1 对象模型和 SQL 模型,两种思路的碰撞
Hibernate 和 MyBatis 虽然都干持久层的活,但底层思路完全不同。Hibernate 是 ORM(对象关系映射)框架,核心是"你操作对象,它帮你翻译成 SQL";MyBatis 则是 SQL 映射框架,核心是"你写 SQL,它帮你把结果集塞回对象"。打个不严谨的比方:Hibernate 像请了个自动挡司机,你只说要到哪,剩下的交给它;MyBatis 更像手动挡,换挡时机永远在你手里。
这个区别决定了代码风格。Hibernate 的项目里,实体类往往就是一张表,你调用repository.save(user),框架会自动生成 INSERT;而在 MyBatis 项目里,你至少要写一条<insert>SQL,再配上resultMap或注解。数据量大、SQL 极其复杂的报表场景,MyBatis 更灵活;但普通业务系统的增删改查,Hibernate 的开发效率明显更高。
1.2 为什么这个例子更适合 Hibernate
我这边的需求很典型:一张用户表,几个字段,加查询分页。这种场景用 Hibernate 有几个实打实的优势:
JpaRepository内置了findAll、findById、save、deleteById等通用方法,不需要为每个实体重写基础 CRUD。- 表结构在开发初期变动频繁,Hibernate 的
ddl-auto=update能自动同步表结构,省掉手工执行 ALTER TABLE。 - 实体关系(一对多、多对多)的映射声明式完成,省去大量 JOIN SQL 维护成本。
当然,Hibernate 也有学习难点,比如缓存、延迟加载、命名策略。但在"简单例子"这个定位下,性价比很高。
1.3 自动建表这件事,MyBatis 能做但不是默认功能
网上经常看到"Spring Boot + MyBatis 当表不存在自动建表"的提问,说明自动建表是刚需。MyBatis 要实现这个效果,一般有两种做法:一是靠 Spring Boot 的 SQL 初始化脚本(schema.sql+spring.sql.init.mode),二是手动写一个启动监听器去判断执行建表 SQL。都能做,但多少要自己搭两下。
Hibernate 这边是天然能力:实体类写好,配置文件里把ddl-auto设成update,启动时框架自动检查表结构,缺表建表、缺列加列。这个"内建"和"拼接"的差别,就是很多人选 Hibernate 的首要理由。
2. 环境和依赖先钉死:版本不匹配是最大的隐形坑
2.1 Spring Boot / JDK / Hibernate 版本对照
版本问题是新手最容易翻车的地方。很多老教程还在用javax.persistence和mysql-connector-java,放到 Spring Boot 3 项目里直接编译不过。我整理了一个对应表,建议先对着表定版本再动手。
| Spring Boot | 最低 JDK | 内置 Hibernate | JDBC 驱动坐标 |
|---|---|---|---|
| 2.7.x | JDK 8 | Hibernate 5.6.x | mysql:mysql-connector-java:8.0.x |
| 3.0 ~ 3.2 | JDK 17 | Hibernate 6.2 ~ 6.4 | com.mysql:mysql-connector-j |
| 3.3+ | JDK 17 | Hibernate 6.5+ | com.mysql:mysql-connector-j |
所谓"Spring Boot 版本太高"的报错,九成是两种:JDK 8 跑 Spring Boot 3,启动时报Invalid target release或者UnsupportedClassVersionError;另一种是把旧教程里的javax.persistence.*原样粘进来,产生一系列找不到类的编译错误。解决方式很简单——用新项目就接受新命名空间,所有javax.persistence改成jakarta.persistence;不想改就老老实实降回 Spring Boot 2.7。
2.2 pom.xml 里最容易抄错的两个坐标
先贴一份最小可用依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</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>注意两点。第一,不需要单独引入hibernate-core,spring-boot-starter-data-jpa会把 Hibernate 的版本管理好;自己强行加低版本 Hibernate 反而可能和 Spring Boot 冲突。第二,Spring Boot 3 里 JDBC 驱动坐标已经是com.mysql:mysql-connector-j,旧坐标在 managed dependencies 里已经失效,别再从老文章里复制mysql:mysql-connector-java。
2.3 application.yml 的完整配置,以及每个参数为什么存在
spring: datasource: url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8 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 open-in-view: false这里每个参数都有来头:
useSSL=false:本地开发没必要走 SSL,否则 MySQL 8 默认可能提示 SSL 连接告警。serverTimezone=Asia/Shanghai:驱动和 MySQL 之间时区不一致时,LocalDateTime会偏差 8 小时,显式指定后才不会踩"时间比实际多/少 8 小时"的坑。allowPublicKeyRetrieval=true:MySQL 8 默认认证插件是caching_sha2_password,初次连接时需要获取 RSA 公钥,不放开这个参数会报Public Key Retrieval is not allowed。ddl-auto=update:让 Hibernate 启动时自动建表/更新表结构。只做本地演示时可以这样,生产环境最好改回validate或none。show-sql=true:控制台打印生成的 SQL,排查问题的第一步永远是"先看它到底执行了什么"。open-in-view=false:关闭 OSIV,避免一个 HTTP 请求全程持有数据库 Session,也逼自己尽早养成用 DTO 或显式查询的意识。
2.4 本地 MySQL 连不上的两个高频原因
一个是error 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock',这个问题常见于本机用 socket 方式连 MySQL,但服务没起来,或者 socket 路径不对。先确认 MySQL 服务端在跑,再确认你用的不是 TCP 端口,就能排查大半。
另一个是应用启动报Communications link failure,这多半是 MySQL 端口没开、服务没监听,或者url里的数据库名demo根本不存在。记住一个事实:JDBC 初始化jdbc:mysql://localhost:3306/demo时,如果demo库不存在,你会先看到Unknown database 'demo'而不是建表相关报错。所以最简单粗暴的办法是先手动创建空库,再交给 Hibernate 建表。
3. 三行注解搭好数据层:实体映射和 Repository
3.1 Entity 写的对与不对,表结构一眼见分晓
写实体类是目前 Hibernate 项目里最接近"先设计表"的步骤。一个最简用户实体长这样:
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 = "username", length = 50, nullable = false) private String username; @Column(name = "age") private Integer age; @Column(name = "create_time") private LocalDateTime createTime; public User() { } // getter / setter 略 }几个值得留意的点:
@Table(name = "t_user")为什么加t_前缀?因为user在 MySQL 里和系统表、函数有潜在命名冲突,加前缀是成本最低的规避方式。@GeneratedValue(strategy = GenerationType.IDENTITY)对应 MySQL 的AUTO_INCREMENT,这是 MySQL 下最自然的自增策略。- 无参构造必须保留,Hibernate 通过反射创建对象时依赖它。
LocalDateTime直接映射 MySQL 的datetime类型,比早年的java.util.Date好用得多。
3.2 Repository:方法名派生查询、@Query 和分页
实体写好后,数据访问层只需要一个接口:
package com.example.demo.repository; import com.example.demo.entity.User; 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.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface UserRepository extends JpaRepository<User, Long> { List<User> findByAgeGreaterThan(Integer age); @Query("select u from User u where u.username like concat('%', :kw, '%')") List<User> searchByKeyword(@Param("kw") String keyword); Page<User> findAll(Pageable pageable); }第一种findByAgeGreaterThan是派生查询,Spring Data JPA 会根据方法名自动生成 SQL,适用于简单条件。第二种@Query是 JPQL,本质是面向实体和属性写查询,比手写原生 SQL 多了一层对象抽象,复杂查询更可控。第三种findAll(Pageable)直接返回分页对象,配合PageRequest.of(0, 10)就能得到Page,里面涵盖总条数、当前页数据、总页数,接口层直接返回前端省事。
3.3 命名策略:create_time 还是 createTime,必须早做决定
这是 Hibernate 项目里一个很经典的混淆点。Spring Boot 2.x 之后,默认的物理命名策略是CamelCaseToUnderscoresNamingStrategy,也就是实体属性createTime会自动映射到表的create_time列,不用手动指定@Column(name = "create_time")。
但要注意:如果某天为了性能或遗留表,你手动建了表,列名刚好叫createTime(大小写混合),MySQL 在 Linux 下对列名大小写敏感,Hibernate 生成的 SQL 按默认策略找create_time,就会报Unknown column 'u0_.create_time'。所以我的建议是:要么把命名策略说明白,要么所有字段都用显式@Column把列名钉死,别让 Hibernate 猜。项目里统一一种写法,比争论哪种更优雅有意义得多。
4. 从 Service 到 Controller:一次完整的增删改查闭环
4.1 @Transactional 的意义
数据层写完之后,别急着把Repository直接塞给 Controller,中间加一层 Service 是有必要的:
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } @Transactional(readOnly = true) public List<User> findAll() { return userRepository.findAll(); } @Transactional public User createUser(User user) { return userRepository.save(user); } @Transactional public void deleteUser(Long id) { userRepository.deleteById(id); } @Transactional(readOnly = true) public Page<User> findPage(Pageable pageable) { return userRepository.findAll(pageable); } }@Transactional的核心作用有两个。第一,save本身虽然也能直接执行,但一旦方法里有多个写操作,事务注解能让它们要么全成功、要么全回滚,不会出现数据写了一半的尴尬状态。第二,Hibernate 的延迟加载必须发生在 Session 存活期间,事务还没结束时访问关联对象才安全;凡是涉及懒加载的查询,不在事务方法里做就会看到LazyInitializationException。
4.2 一个 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; } @GetMapping public List<User> list() { return userService.findAll(); } @PostMapping public User create(@RequestBody User user) { return userService.createUser(user); } @DeleteMapping("/{id}") public void delete(@PathVariable Long id) { userService.deleteUser(id); } }这个 Controller 用了@RestController,所有方法返回值直接写进 HTTP 响应体,不再需要@ResponseBody。这里有个小提醒:返回实体对象本身在演示阶段没问题,但正式项目里最好返回 DTO,否则实体内部的关联字段、敏感字段可能被序列化出去,而且懒加载字段在事务外访问会爆炸。
4.3 用 curl 验证接口
项目启动后,可以用最原始的方式验证闭环:
# 创建用户 curl -X POST http://localhost:8080/api/users \ -H "Content-Type: application/json" \ -d '{"username":"张三","age":25}' # 查询列表 curl http://localhost:8080/api/users # 删除用户 curl -X DELETE http://localhost:8080/api/users/1配合show-sql: true,你能在控制台看到 Hibernate 实际执行的 INSERT / SELECT / DELETE。看到 SQL 和控件台预期一致,这个简单例子就算真的跑通了。
5. 跑通之后的四类报错,帮你把排查顺序理顺
演示项目跑通只是开始,真正的收获往往在报错里。我把最常见的几类报错和排查顺序整理成一张表:
| 报错表现 | 根本原因 | 处理方式 |
|---|---|---|
Table 'demo.t_user' doesn't exist | ddl-auto是none或validate,Hibernate 不建表;或连接的是另一个 schema | 检查ddl-auto是否为update,确认url里的库名正确 |
Unknown column 'u0_.create_time' | 实体属性是createTime,但表里列名是createTime(大小写不一致)或手动建表时列名不对 | 统一命名策略,或显式写@Column(name = "create_time") |
Access denied for user 'root'@'localhost' | 密码错误,或 MySQL 8 认证插件不兼容 | 先用mysql -uroot -p本地登录验证;必要时调整认证插件 |
Public Key Retrieval is not allowed | caching_sha2_password初次连接需要公钥 | 在url后加allowPublicKeyRetrieval=true,或改回mysql_native_password |
编译报错找不到javax.persistence.* | 使用了 Spring Boot 3,老教程导包是旧命名空间 | 全局替换javax.persistence为jakarta.persistence |
5.1 Table doesn't exist / Unknown table 的排查思路
遇到建表相关报错,先别急着重启。按这个顺序问自己:数据库有没有连错?ddl-auto是不是update?实体类有没有被扫描到?通常后两个问题最快验证:看一眼启动日志里有没有 Hibernate 生成的Hibernate: create table t_user日志,如果没有,多半是配置或扫描路径问题。另外,ddl-auto的五个取值create、create-drop、update、validate、none要分清楚。create每次启动都删表重建,create-drop是每次结束删表,update只增量更新,validate只校验不修改,none完全不参与表结构。
5.2 Unknown column 和命名策略的关系
这个报错其实藏了一个设计问题:到底由谁决定列名?Hibernate 默认策略是把实体属性从驼峰转下划线,这是约定;但如果你手动建表时用了createTime这种带大小写的列名,到了 Linux 上的 MySQL 就会因为大小写敏感对不上。最稳妥做法是:实体字段 + 表列名全部小写下划线,统一交给默认策略转换,或者全部显式声明@Column。两种都可以,最怕的是"一半靠猜、一半靠写"。
5.3 Access denied 与 Public Key Retrieval 的先后顺序
这两个报错经常混在 MySQL 8 的语境里出现。Access denied说明认证直接被拒,重点查密码;Public Key Retrieval is not allowed说明密码验证通过了,只是后续密钥交换被安全参数拦下。所以排查顺序一定是先保证mysql -uroot -p本机命令行能登录,再谈 URL 里的allowPublicKeyRetrieval。如果本机命令行都进不去,改连接串参数没有意义。
5.4 Spring Boot 3 的换包问题
最后提醒一次:现在网上还有大量 Hibernate 教程是针对 Spring Boot 2.x 写的,导入包是javax.persistence。但 Spring Boot 3 全面转向 Jakarta EE 9,命名空间已经变成jakarta.persistence。如果你项目里出现上百个红色报错,先别怀疑代码逻辑,把 pom 里的版本和 import 路径检查一遍,往往一分钟解决。
大概就是这些。最后分享一个我自己的习惯:本地开发我很少直接装 MySQL,而是用 Docker 一条命令起一个干净的实例,docker run --name mysql8 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=demo -p 3306:3306 -d mysql:8.0,这样版本统一,删了重建也方便,能少踩很多操作系统差异带来的坑。另外,如果项目最终要上线,记得把ddl-auto从update改成validate或none,由审过的建表脚本保证表结构,否则生产环境上悄悄给你加一列,你连自己都不知道。
本文还有配套的精品资源,点击获取