☰
Spring Boot整合Hibernate实战:SessionFactory配置与CRUD完整示例
2026/10/29 19:17:05 网站建设 项目流程

简介:面向Spring Boot初学者的入门级实例,演示整合Hibernate与MySQL完成基础插入和查询操作。项目自带完整依赖jar包与建表SQL脚本,创建对应数据库并执行脚本即可运行,适合想快速跑通ORM流程的开发者。压缩包共155个文件,含64个jar依赖、6个Java源码、6个class文件、3个properties配置、2个JSP页面及1个SQL脚本,整体约34.25MB,便于按目录对照学习。已有1826人学习下载。通过该项目可掌握Spring Boot项目结构、Hibernate实体映射与Session工厂配置、MySQL连接池设置等要点,同时获得一份可直接复用和修改的入门骨架,有效降低后续开发的环境搭建成本。

1. 为什么还在用Hibernate:这个简单例子解决的是什么

面试被问到“Hibernate还有人用吗”时,多数人第一反应是拿Spring Data JPA来挡,但真正维护过老项目的工程师都知道,SessionFactory、HibernateTransactionManager这套原生API才是分水岭。这份“springboot+hibernate+mysql简单例子”恰恰是一个最小可运行的CRUD工程,核心解决的是“Hibernate的SessionFactory在Spring Boot里到底怎么管”的问题:谁来创建、谁负责事务、实体映射落在哪里。它适合两类人:一是手上要维护遗留项目但找不到完整配置示例的从业者,二是面试前想快速把Hibernate事务边界和映射注解过一遍的开发者。与其在网上零散翻配置片段,不如直接拿一套能跑通的源码对照着改。

2. 环境与版本:开发前先定死这四件事,否则跑不起来

这个例子本身不复杂,但环境版本要是没对齐,光是启动就能卡住一晚上。我在拆这份资源时,第一条血泪经验就是:先把JDK、MySQL、Maven、Spring Boot版本锁定,再动代码。环境变量、驱动类名、方言自动检测、时区处理,这四个地方任何一个跟你本机不一致,都会报出看起来莫名其妙的错。

2.1 MySQL 5.7还是8.0:这个例子的最佳选择

网络上的mysql安装教程铺天盖地,但真正决定你跑不跑得动的是版本选择。这套例子里我优先建议用MySQL 5.7.44,而不是8.0.x。原因不是性能,而是坑最少。

Windows 10环境下的安装过程很简单:下载msi安装包后一路Next,选Server only,端口默认3306,字符集选utf8mb4,最后设置root密码。装完以后在命令行里验证一下:

mysql --version mysql -uroot -p

第一条命令确认版本,第二条连接数据库。连接成功后再执行show variables like 'character_set%';,确认字符集是utf8mb4。

这里有一个容易忽略的版本差异:MySQL 5.7默认认证插件是mysql_native_password,MySQL 8.0默认是caching_sha2_password。如果用8.0,连接串里必须带allowPublicKeyRetrieval=true,否则JDBC首次连接时没有缓存公钥,直接报Public Key Retrieval is not allowed。

对比项MySQL 5.7.44MySQL 8.0.x
JDBC驱动类com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver
默认认证插件mysql_native_passwordcaching_sha2_password
时区默认值系统时区UTC,需要显式指定serverTimezone
内存占用约300MB左右更重,虚拟内存占用翻倍
方言配置MySQL5InnoDBDialectMySQL8Dialect

如果本机已经装了8.0,不需要卸载,只要把驱动和方言对应改掉,例子一样能跑。但如果你是第一次搭,别给自己找麻烦。

2.2 Maven项目结构与依赖声明:版本对应关系

这个例子的工程结构是标准的Maven单模块布局,没有花活。我建议你直接用IDE导入后跑,而不是手动创建一个空工程再贴代码。下载资源里已经包含了完整的src目录和pom.xml,你只需要保证Maven版本在3.6以上。

先说结构,避免新手对着文件夹发懵:

springboot-hibernate-mysql/ ├── pom.xml └── src/main/ ├── java/com/example/demo/ │ ├── DemoApplication.java │ ├── config/HibernateConfig.java │ ├── entity/User.java │ ├── dao/UserDao.java │ └── service/UserService.java └── resources/ └── application.yml

pom.xml里最核心的依赖就三块,每一块都有版本陷阱。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

这里的逻辑是:spring-boot-starter-data-jpa默认依赖Hibernate,而Spring Boot 2.7.x对应的Hibernate版本是5.6.x,用的事javax.命名空间,不需要改动任何import。常见翻车是springboot版本太高,比如直接上Spring Boot 3.2.x,Hibernate跟着升级到6.x,命名空间全部变成jakarta.,群里贴的javax.persistence注解全报红。

MySQL驱动版本也同理:MySQL 5.7配5.1.49,MySQL 8.0配8.0.33。把5.1.49的驱动拿去找8.0数据库,连SSL握手都会出问题;反过来把8.0驱动用在5.7上,虽然能连上,但驱动类名已经是com.mysql.cj.jdbc.Driver了,配置里不改就报ClassNotFound。

2.3 application.yml配置:连接串、方言与时区缺一不可

配置是整个例子里最容易被复制出错的部分。下面是这份资源里自带的application.yml,我逐行拆开讲为什么这么写。

spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5InnoDBDialect format_sql: true

这几个参数每一个都有故事。

driver-class-name必须和Maven里的驱动版本匹配,用了5.x驱动就写com.mysql.jdbc.Driver,用了8.x驱动就写com.mysql.cj.jdbc.Driver。实际中有个更省事的原则:Spring Boot会从jdbc:mysql://前缀自动探测驱动类,所以这一行直接删掉也完全没问题,除非你是老项目迁移历史遗留了这行配置。

url里serverTimezone=Asia/Shanghai是MySQL 8.0的必需品,5.7环境下不写也能跑,但写上不会出错。useSSL=false是本地开发铁律,不然MySQL 8.0在握手阶段会尝试做SSL认证,偶尔能连上偶尔直接超时,属于黑匣子问题。

dialect这里要特别小心。Spring Boot 2.7.x的Hibernate 5.6版本已经支持方言自动检测,理论上不配也能跑。但自动检测偶尔会把MySQL 8识别成MySQL8Dialect,如果你的驱动和数据库版本不匹配,就会在启动时因为方言生成的SQL语法不通报出Type not recognized。手动指定MySQL5InnoDBDialect后,分页、建表、类型映射都会走InnoDB语义,和ddl-auto: update配合最稳。

ddl-auto: update的目的是让Hibernate根据实体类自动建表和更新表结构,第一次跑的时候自动创建user表,以后每次启动做增量比对。如果改成create,每次重启都会先drop再create,日常调试可以,但你要是写入了测试数据就会被清空。真正上线前,这个值应该换成none或validate。

3. 把Hibernate接进Spring Boot:SessionFactory管理、实体映射与CRUD实现

这一章是整套例子的核心。很多人用Spring Data JPA时完全不感知Hibernate的存在,但这份资源回归了Hibernate原生API:手动配置LocalSessionFactoryBean、注册HibernateTransactionManager、从SessionFactory拿Session做CRUD。这套流程不花哨,但恰好复现了老项目里最常见的整合方式。

3.1 SessionFactory为什么不用手动创建

只讲原理不讲怎么办那是耍流氓。先记住一点:SessionFactory是进程级单例,创建成本极高,内部持有连接池和二级缓存元数据,绝对不能每次操作都new一个。Spring Boot里的正确做法是交给容器,由LocalSessionFactoryBean创建并管理它的生命周期。

@Configuration public class HibernateConfig { @Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setPackagesToScan("com.example.demo.entity"); Properties props = new Properties(); props.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect"); props.setProperty("hibernate.show_sql", "true"); props.setProperty("hibernate.hbm2ddl.auto", "update"); factoryBean.setHibernateProperties(props); return factoryBean; } @Bean public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(sessionFactory); return txManager; } }

这两段配置的逻辑很清晰:sessionFactory方法接收Spring Boot自动配置的DataSource,然后把实体扫描范围限定在com.example.demo.entity包,最后把方言、建表策略这些Hibernate属性塞进去。transactionManager方法把同一个SessionFactory交给HibernateTransactionManager,这样@Transactional注解才能真正控制Hibernate的事务提交和回滚。

有一个细节值得注意:HibernateTransactionManager绑定的是线程级别的Session,所以代码里拿Session应该用sessionFactory.getCurrentSession(),而不是openSession()。两者的区别在于,getCurrentSession()会复用当前事务绑定的Session,当事务结束时自动关闭;openSession()每次都创建一个新连接,用完了不手动close就连接泄露。

3.2 实体类与表映射:注解比XML更适合新手

老一代Hibernate项目里经常看到大段.hbm.xml映射文件,但这套例子用的是JPA注解,维护成本低得多。实体类是跟数据库表一一对应的映射模型,字段类型、约束、自增策略全在这里声明。

@Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 64) private String username; @Column(nullable = false, length = 64) private String password; @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; // 省略getter/setter }

@GeneratedValue(strategy = GenerationType.IDENTITY)对应MySQL的自增主键AUTO_INCREMENT,这是MySQL下的最优解。如果用了GenerationType.AUTO,Hibernate会尝试创建一张hibernate_sequence表来维护ID生成,显得画蛇添足。

@Column(name = "created_at", updatable = false)里的name显式指定列名,避免把Java的驼峰命名createdAt直接映射成createdAt,MySQL在Linux下区分大小写容易出幺蛾子。updatable = false说明这条记录一旦插入就不再允许更新,这是一个很小的防呆设计。

写实体类时还要注意Java 8时间类型。Hibernate 5.6之后对LocalDateTime的映射已经很好,不需要@Temporal(TemporalType.TIMESTAMP)这个历史注解。MySQL端对应的是datetime类型,Hibernate会根据方言自动完成映射。

3.3 Service层实现CRUD:事务边界与查询写法

Dao层直接使用SessionFactory,Service层负责包事务。这个分层方式跟JPA的Repository风格明显不同,但更接近Hibernate原生生态的习惯。

@Repository public class UserDao { @Autowired private SessionFactory sessionFactory; public User findById(Long id) { Session session = sessionFactory.getCurrentSession(); return session.get(User.class, id); } public User findByUsername(String username) { Session session = sessionFactory.getCurrentSession(); Query<User> query = session.createQuery("from User where username = :username", User.class); query.setParameter("username", username); return query.setMaxResults(1).uniqueResult(); } public void save(User user) { Session session = sessionFactory.getCurrentSession(); session.save(user); } }
@Service public class UserService { @Autowired private UserDao userDao; @Transactional public void createUser(String username, String password) { User user = new User(); user.setUsername(username); user.setPassword(password); user.setCreatedAt(LocalDateTime.now()); userDao.save(user); } @Transactional(readOnly = true) public User getUserByUsername(String username) { return userDao.findByUsername(username); } @Transactional public void updatePassword(Long id, String newPassword) { User user = userDao.findById(id); user.setPassword(newPassword); } }

这段代码里最容易被忽略的是查询里没有写select *也没有写表名,而是from User。Hibernate的HQL查询操作的是实体类,不是数据库表,所以from User where username = :username里的User对应的是User.class实体。setParameter是参数绑定,不是字符串拼接,这一点能从根上防SQL注入。

另一个点是@Transactional(readOnly = true)。只读事务会告诉Hibernate不需要做脏检查,不会在事务提交时执行多余的Update语句。对一次findByUsername来说,性能差异微乎其微,但如果查询逻辑复杂、关联表多,这个习惯能减少不必要的flush操作。

如果你刚开始接触这套原生API,记住一句话:Dao层只有数据库操作,不带事务;Service层才加@Transactional。事务加错位置,比如加到Dao层,会导致Controller直接调Dao时事务提前提交,第二次查询走的是已失效的快照数据。

4. 避坑与排查:CRUD跑通之前的五个典型问题

这一章是我拆这套例子过程中反复踩过的坑,每一条都对应明确的现象、原因和解决路径。你把例子里代码原样跑起来可能没事,但一旦改动环境,下面这些坑就轮番登场了。

4.1 Communications link failure:时区导致的第一次断电

现象:启动时数据源初始化失败,最后一行的错误是The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone。有些机器上还会伴随后面的Communications link failure。

原因:MySQL驱动6.0之后强制要求指定时区,因为JDBC连接需要知道服务器时区才能正确转换TIMESTAMP类型。MySQL 5.7默认读取系统时区,中文Windows系统返回的是中文时区描述,JDBC解析不了;MySQL 8.0默认UTC,不指定时区会差8小时。

解决:统一在url末尾加serverTimezone=Asia/Shanghai,同时建议把MySQL系统时区也设置为东八区。这是一个配置层面就能直接解决的坑,不用动代码。

url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai

4.2 ClassNotFound:驱动类名的历史遗留问题

现象:报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。

原因:你用的mysql-connector-java是8.x版本,驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。老教程里写的类名已经随着版本升级迁移了。

解决:两个办法任选其一。要么把pom.xml里的驱动版本降回5.1.49,继续用老类名;要么把application.yml里的driver-class-name改成新类名。我倾向于推荐后者,毕竟用新版本驱动本身没有坏处,而且Spring Boot的自动探测机制足够聪明,你甚至可以直接删掉driver-class-name这行配置。

4.3 方言不对导致建表失败:Type not recognized

现象:ddl-auto: update开启后,Hibernate启动时报Unsupported column type或者Type not recognized,错误信息里能看到它尝试生成的DDL语句用了不存在的类型名。

原因:Hibernate方言表决定了它把Java类型映射成MySQL哪种数据类型。MySQL5InnoDBDialect和MySQL8Dialect在时间类型、JSON类型上映射规则不同,如果你用的是8.x方言配置文件配的5.7数据库,或者反过来,就会出现类型映射错乱。最常见的翻车场景是从网上复制了一个适用于MySQL 5.5的配置,方言写成MySQLDialect,这个方言不对应InnoDB,Hibernate生成的表引擎是MyISAM,既不支持事务也不支持外键约束。

解决:锁定版本后固定方言。这个例子默认用MySQL5InnoDBDialect+ MySQL 5.7的组合,如果你用MySQL 8.0,改成org.hibernate.dialect.MySQL8Dialect。注意不是光改方言就完事,驱动版本要同步对齐。

4.4 每次请求都创建SessionFactory:把单例写成了工厂

现象:项目能启动,但每执行一次CRUD就停顿几秒,日志里能看到Building new Hibernate SessionFactory,连接池反复重建,运行一段时间后数据库连接数飙升。

原因:有人把Configuration().configure()写在了工具类里,每次调用都重新加载Hibernate配置文件并构建一个新的SessionFactory。SessionFactory是重量级对象,线程安全、进程级单例,它内部有连接池、二级缓存、预编译SQL等状态,频繁创建既慢又浪费连接。

解决:强制走HibernateConfig里定义的@Bean,从Spring容器注入SessionFactory。如果你一定要在工具类里用,就搞一个静态Holder持有单例,不要每次new。

4.5 更新完查还是旧值:事务没包住查询

现象:Service里先调updatePassword,紧接着在同一线程里调getUserByUsername,读出来的还是旧密码。日志里能看到Update语句确实执行了,但查询结果像被缓存了一样。

原因:Hibernate的getCurrentSession()依赖线程绑定事务,如果Dao层方法没有处于一个事务上下文中,getCurrentSession()会直接报错或者拿到一个脱离了事务的Session。更隐蔽的是,查询方法上没加@Transactional,Session自己开了个事务执行查询,执行完就提交关闭了,跟前面的更新根本不在同一个Session里,一级缓存没有命中同一条记录。

解决:查询和更新应该落在同一个Service事务方法里,或者确保查询方法本身加了@Transactional(readOnly = true)。另外,不要在实体上手动调用session.flush()后期待马上刷新,flush只是把SQL发到数据库,要提交事务后别的Session才能读到新值。

5. 让例子再进一步:事务边界、N+1查询与一个验证习惯

基础CRUD跑通以后,这个例子还有两个可以立刻上手验证的点:事务边界和N+1查询。这两个概念在面试和代码评审里出现的频率非常高,用这个最小工程去验证比死记结论要扎实得多。

先说N+1查询。给实体加一个一对多关联,比如用户有多条地址记录,在查询用户列表时如果只查用户本身,Hibernate默认对关联集合使用懒加载。当你循环遍历用户列表去访问每个用户的地址时,Hibernate会为每个用户额外执行一条查询,最终执行了1条主查询加N条子查询。

@Entity @Table(name = "user") public class User { // 省略其他字段 @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Address> addresses; }

这时候开启show-sql,在日志里能清清楚楚看到第一条是select ... from user,后面循环里跟着N条select ... from address where user_id=?。解决办法是查询里显式join fetch,一次性把子表数据查出来,这比把fetch改成EAGER更可取,因为后者会在任何场景下无条件加载子表数据,反而制造新的性能问题。

事务边界的验证更简单。在Service方法上标@Transactional,然后在方法里执行两次查询,检查Hibernate一级缓存的行为。你也可以故意在方法中途抛一个RuntimeException,观察之前的更新操作是否回滚。需要记住的是Spring Boot默认使用CGLIB代理实现事务,这意味着@Transactional方法必须是public,并且不能通过同类内部的this调用来触发,否则代理不生效,事务静默失效。

至于验证习惯,我每次拿到一套Hibernate相关资源,第一件事不是跑通CRUD,而是打开show-sql确认SQL执行路径。show-sql输出的每一行Hibernate:前缀日志,都告诉你Hibernate实际发到数据库的SQL长什么样。如果只看到一条SQL,说明参数绑定和方言都没问题;如果一眼扫过去有十几条重复结构的SQL,一定是懒加载触发了N+1。

把这个开关从true调成false只需要改一行配置,但它在排查时的价值远超那些日志本身的性能消耗。从那以后我每次把例子拿给别人跑,都强制要求先开show-sql确认SQL执行路径,再关掉这个开关收尾。

希望你拿到这套springboot+hibernate+mysql例子后,不要只盯着“能跑”这个结果,而是花一下午把show-sql打开,逐条看一遍它发出的SQL,再对照这篇笔记里的版本选型和事务边界去调参。一套代码吃掉原理之后,它能变成你后面排查老项目时随手可用的工具。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询