☰
Spring Boot非JPA整合Hibernate:SessionFactory原生配置实战
2026/10/17 9:56:28 网站建设 项目流程

说实话,每次有人在群里问“Spring Boot 能不能不用 JPA 那套,直接用 Hibernate”,我都会先反问一句:你说的“JPA”是指 Spring Data JPA,还是指底层的 EntityManager?因为大多数教程把 Hibernate 和 JPA 彻底捆绑在一起讲,导致很多人误以为只要用 Hibernate 就得走持久化上下文那套规范。其实完全不是一回事。

我手上的一个老项目,用 Spring Boot 3.x 重新梳理数据层,团队明确要求不要引入 Spring Data JPA,也不要走EntityManager,而是希望用org.hibernate.Session/SessionFactory这套原生 API。当时网上找到的文章要么是教你怎么配LocalContainerEntityManagerFactoryBean,要么就是用 Spring Data 的CrudRepository,真正“非 JPA”的注解配置案例少得可怜。这篇文章就是我整理完那个项目后留下的笔记。

标题里的“非 JPA”大家先别理解偏了:实体上的@Entity、@Table这种注解照样用,因为 Hibernate 6 已经完全基于 Jakarta Persistence 注解来驱动映射。真正的区别在于——我们不创建EntityManagerFactory,不依赖 Spring Data 的 repository,而是自己声明LocalSessionFactoryBean,将SessionFactory注入业务层,用Session完成增删改查和事务控制。

如果你属于这几类情况,这篇文章值得看完:第一,旧项目从 SSM 或原生态 Hibernate 迁移过来,不想把 DAO 层推到重写;第二,需要用到 Hibernate 自带的@Where、@Formula、@FilterDef、@SQLDelete这类专属能力,而这些用标准 JPA 会非常别扭;第三,团队本身对 HQL 和原生 SQL 更熟,不想被 repository 的封装挡住调试视线。

1. 为什么“非 JPA”仍然值得做?先搞懂 Hibernate 和 JPA 的分工

1.1 JPA 是规范,Hibernate 是实现,但实现明显比规范多

很多人以为 Hibernate 就是 JPA,JPA 就是 Hibernate。严格说,JPA 是一套由jakarta.persistence包定义的规范,它规定了实体映射、EntityManager、JPQL、事务等标准接口;Hibernate 是这套规范最常见的实现之一,但 Hibernate 在实现规范之外,又额外提供了一大堆自己的 API 和注解。

所以当你用原生 Hibernate 时,@Entity、@Table、@Id这些仍然是 Jakarta Persistence 的注解,不是 Hibernate 发明的。真正属于 Hibernate 的,是org.hibernate.annotations这个包下的东西,比如@Formula、@Where、@GenerationTime、@NaturalId等等。“非 JPA”这句话,更准确的理解是:基础注解跟随 JPA 标准,但 API 和容器装配全程走 Hibernate 自己的 SessionFactory。

1.2 什么项目会主动绕开 EntityManager?

先说一个最常见的业务场景。某个运营系统原先用的是 SSH 或者 SSM 三段式架构,DAO 类里到处是sessionFactory.getCurrentSession(),整整积累了七八年。现在要迁到 Spring Boot,如果硬切 Spring Data JPA,等于要把几十个 DAO 全部重写一遍,而且还得重新梳理事务边界。这种项目里,原生 Hibernate 天然是嫁接最平滑的方案。

再说一个技术选型上的理由。标准 JPA 对实体的控制力度偏“标准”,而业务上常见的一些骚操作,比如软删除的时候把DELETE语句改成逻辑删除的UPDATE语句,或者某个字段不落到数据库表里、而是用 SQL 子查询实时算出来,这些能力用 Hibernate 自带的注解可以写得很优雅,放在纯 JPA 标准里就只能靠事件监听器或者自己造轮子。

还有一些情况是团队技术栈决定的。有些人调 HQL 调习惯了,一条复杂查询怎么写、缓存怎么控制,心里门清;但换到 JPA 的 Criteria 或者 Spring Data 的 Specification,总觉得绕了一层布,排查问题时还得解包。信息透明带来的调试效率,有时候比“标准”本身更值钱。

1.3 反过来也要劝退一些人

不是所有项目都适合非 JPA。如果你的业务只是简单 CRUD,甚至百分之八十的查询都是单表操作,那我强烈建议直接使用 Spring Data JPA。它的 automatic implementation 能把 DAO 代码量压缩到令人发指的地步,而且社区资料极多,新手上手成本低。原生 Hibernate 适合的是“资源可控、踩坑能力足够”的团队。盲目跟风原生 API,等于主动放弃了一个成熟生态里的省心配置。先把适用边界划清楚,后面实操起来心里才有底。

2. 依赖清单与自动配置:第一步最容易翻车

2.1 Maven 里别再引入 starter-data-jpa

既然目标是非 JPA,spring-boot-starter-data-jpa这个依赖最好直接不出现。它不是 JPA 规范本身,而是 Spring Data 在 JPA 之上的自动配置包,里面包含了JpaRepositoriesAutoConfiguration、HibernateJpaAutoConfiguration一堆自动装配逻辑,我们并不需要它们。

我当时的最小依赖是这样列的。

<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-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> </dependency> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这里三个细节很关键,很多人就是栽在这里。

第一个细节是spring-boot-starter-jdbc不能省。虽然我们用的是 Hibernate,但数据源本身还是要 Spring Boot 的DataSourceAutoConfiguration来创建,它会给一个 HikariCP 连接池。hibernate-core不会自动帮你管理连接池,它只知道从DataSource拿连接。

第二个细节是spring-orm必须手工加。LocalSessionFactoryBean不是 Hibernate 里的类,而是 Spring 对它做的集成封装,位于org.springframework.orm.hibernate5或者新版里的org.springframework.orm.hibernate6包下。如果你只依赖了hibernate-core,没有spring-orm,编译期直接找不到这个类。

第三个细节是版本号。spring-boot-starter-parent里已经锁定了 Hibernate 6.x 的版本,所以hibernate-core不用写<version>。这是大多数人容易忽略的地方,手动指定版本反而可能跟 Spring Boot 内部的依赖管理打架。

2.2 排除自动配置的时候,别连数据源自动配置一起干掉了

有了spring-orm和hibernate-core,类路径上已经同时出现了LocalContainerEntityManagerFactoryBean和EntityManager,Spring Boot 的HibernateJpaAutoConfiguration会选择自动配置一个EntityManagerFactory。我们要明确告诉 Spring Boot:我不需要你替我把 JPA 容器搭起来。

在启动类上做排除是最快的办法。

package com.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

注意,这里只需要排除HibernateJpaAutoConfiguration。JpaRepositoriesAutoConfiguration在没有 Spring Data JPA 的类路径时根本不会生效,不需要管它。DataSourceAutoConfiguration千万别排除,否则数据源都没了,SessionFactory 更无从接起。

有同学会问:如果我的工程里已经引入了spring-boot-starter-data-jpa,现在突然要切非 JPA,怎么办?理论上你可以在启动类里同时排除掉HibernateJpaAutoConfiguration和JpaRepositoriesAutoConfiguration,但更干净的做法还是把依赖证据从pom.xml里彻底删掉。遗留依赖除了带来自动配置风险,还会影响 Maven 依赖树分析。

2.3 配置文件里只需要数据源

application.yml保持朴素,不需要写任何spring.jpa开头的配置项。

spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver

为什么不用写spring.jpa.hibernate.ddl-auto?因为后面我们会在 Hibernate 配置类里直接设置hibernate.hbm2ddl.auto,那些spring.jpa.*配置项主要消费在 JPA 自动配置链路里,我们的链路不经过那里,写了也是白写。这个细节很容易被网上那些混合教程搞糊涂:照着 JPA 教程写spring.jpa.show-sql=true,结果发现日志里啥也没有,因为配置根本没进 SessionFactory。

3. 配置 SessionFactory 和事务管理器:注解扫描从这一步开始

3.1 声明 LocalSessionFactoryBean,而不是 LocalContainerEntityManagerFactoryBean

这是全文的核心。配置类可以直接把DataSource注入进来,交给LocalSessionFactoryBean使用。

package com.demo.config; import java.util.Properties; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.hibernate.SessionFactory; import org.springframework.orm.hibernate5.HibernateTransactionManager; @Configuration @EnableTransactionManagement public class HibernateConfig { @Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean factory = new LocalSessionFactoryBean(); factory.setDataSource(dataSource); factory.setPackagesToScan("com.demo.entity"); factory.setHibernateProperties(hibernateProperties()); return factory; } @Bean public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) { return new HibernateTransactionManager(sessionFactory); } private Properties hibernateProperties() { Properties props = new Properties(); props.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect"); props.setProperty("hibernate.show_sql", "true"); props.setProperty("hibernate.format_sql", "true"); props.setProperty("hibernate.hbm2ddl.auto", "update"); props.setProperty("hibernate.connection.autocommit", "false"); return props; } }

如果你工程里同时引用了 Hibernate 6 和 Spring 6,包名可能会是org.springframework.orm.hibernate6,具体看 Spring ORM 版本。我的项目用的是 Spring Boot 3.2.5,它内部的 spring-orm 对应的是 hibernate6 扩展,所以实际写的时候我把包名换成了org.springframework.orm.hibernate6.LocalSessionFactoryBean和org.springframework.orm.hibernate6.HibernateTransactionManager。大家根据自己 IDE 里实际 import 到的包名来,别照抄完发现类报红。

setPackagesToScan说的是实体包路径,它会自动扫描该包及其子包下所有带@Entity的类。如果你手头实体类比较零散,也可以用factory.setAnnotatedClasses(UserInfo.class, MemberInfo.class, ...)显式列出来。工程项目里我更建议setPackagesToScan,后面新增实体不用改配置类。

3.2 几个 Hibernate 属性的实际意义

很多人配 Hibernate 属性是照抄,我把几个容易理解偏差的说一下。

hibernate.hbm2ddl.auto=update表示应用启动时会根据实体映射自动更新数据库表结构。开发阶段确实方便,改个字段加个列,重启就能同步。但在生产环境这是个隐患:如果线上库表数据量大,Hibernate 生成的 ALTER 语句很可能不是你要的,更严重的还会把字段类型改掉。我的实践是测试环境用update,生产环境一律validate或none,表结构变更交给数据库迁移脚本统一管理。

hibernate.connection.autocommit=false很多人不写。它是提醒 Hibernate 不要依赖连接的自动提交,事务边界交给 Spring 的事务管理器统一控制。如果不写,某些数据库驱动默认 autocommit 打开,可能出现一个 Session 里执行多条 SQL,各自提交,事务形同虚设。

我额外解释一下为什么事务管理器必须用HibernateTransactionManager而不是 Spring 自带的DataSourceTransactionManager。因为HibernateTransactionManager会把当前 Session 和 Spring 的事务同步器绑定在一起,这样你在业务方法里调用sessionFactory.getCurrentSession()时,拿到的就是当前事务对应的同一个 Session。如果用了DataSourceTransactionManager,Session 和事务各管各的,你就会碰到“no current session”或者“session closed”这类诡异问题。

3.3 用 CommandLineRunner 快速验证实体扫描是否成功

配置写完先别急着写 Service,找一个最简单的验证方式。我一般在启动类里临时注册一个CommandLineRunner,把 SessionFactory 里面已经扫描到的实体打印出来。

package com.demo; import org.hibernate.SessionFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.context.annotation.Bean; @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public CommandLineRunner checkSessionFactory(SessionFactory sessionFactory) { return args -> { sessionFactory.getMetamodel().getEntities().forEach(System.out::println); }; } }

启动之后如果控制台能看到EntityTypeImpl{name='com.demo.entity.UserInfo'}这样的信息,说明LocalSessionFactoryBean已经把注解扫描跑通了。如果什么都打印不出来,先回到两个检查点:实体类是不是真的在setPackagesToScan指定的包下;实体类上有没有@Entity注解。这两个问题我至少见过三个人卡了半天。

4. 实体注解:JPA 标准注解负责骨架,Hibernate 专属注解负责业务魔法

4.1 一个能跑起来的实体类长什么样

还是拿最常见的用户表举例。实体类上的基础注解,比如@Entity、@Table、@Id、@GeneratedValue,实际上全部来自jakarta.persistence包,这一点和非 JPA 并不冲突。

package com.demo.entity; import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.DynamicUpdate; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "user_info") @DynamicUpdate public class UserInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", nullable = false, length = 64) private String username; @Column(name = "email", length = 128) private String email; @Column(name = "created_at", updatable = false) @CreationTimestamp private LocalDateTime createdAt; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } }

注意@CreationTimestamp这个注解来自org.hibernate.annotations,目的是让 Hibernate 在insert的时候自动填LocalDateTime,不用我们在业务代码里手动 set。它和 JPA 里@PrePersist监听器能达到类似效果,但写法简洁得多。

4.2 Hibernate 专属注解的大杀器清单

真正让原生 Hibernate 值得用的,是下面这几个表里列出的注解。它们单独任何一个拿出来,都能少写几十行业务代码。

注解包典型使用场景注意事项
@Whereorg.hibernate.annotations软删除,查询时自动附带 deleted=0 条件对关联集合也能生效,但复杂 HQL join 下容易引起迷惑
@Formulaorg.hibernate.annotations实体里加一个只读的统计字段,例如订单数对应于 SQL 片段,不走缓存,频繁查询要评估性能
@SQLDeleteorg.hibernate.annotations调用 delete 时改成逻辑删除 UPDATE通常和@Where搭配使用
@NaturalIdorg.hibernate.annotations业务唯一键,例如手机号可以用session.byNaturalId()直接加载
@DynamicUpdateorg.hibernate.annotations更新时只生成变化字段的 UPDATE 语句有少量动态 SQL 拼装开销,不适合超高频简单更新
@DynamicInsertorg.hibernate.annotations插入时只对有值的字段生成 INSERTHibernate 6 里语义变化要小心
@CreationTimestamp/@UpdateTimestamporg.hibernate.annotations创建时间、更新时间自动填入不要和数据库默认值混淆
@Fetchorg.hibernate.annotations控制关联集合的加载策略,比如 SUBSELECT使用不当会导致 SQL 数量不可控

随便举一个组合使用案例。会员实体希望做到逻辑删除,同时数据表里顺便存一个会员的累计下单数,下边这种写法就非常干净。

package com.demo.entity; import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.Formula; import org.hibernate.annotations.NaturalId; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "member_info") @SQLDelete(sql = "UPDATE member_info SET deleted = 1 WHERE id = ?") @Where(clause = "deleted = 0") @DynamicUpdate public class MemberInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NaturalId @Column(name = "mobile", nullable = false, length = 20) private String mobile; @Column(name = "real_name", length = 64) private String realName; @Formula("(select count(o.id) from order_info o where o.member_id = id and o.deleted = 0)") private Long orderCount; @Column(name = "deleted") private Integer deleted = 0; @Column(name = "create_time", updatable = false) @CreationTimestamp private LocalDateTime createTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } public String getRealName() { return realName; } public void setRealName(String realName) { this.realName = realName; } public Long getOrderCount() { return orderCount; } public void setOrderCount(Long orderCount) { this.orderCount = orderCount; } public Integer getDeleted() { return deleted; } public void setDeleted(Integer deleted) { this.deleted = deleted; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } }

这个实体类里有三个非常实用的小心机。

第一,@SQLDelete把 Hibernate 的delete操作拦截成UPDATE member_info SET deleted = 1 WHERE id = ?,你再也不用在 DAO 里手动写软删除语句了。@Where又保证普通查询自动带上deleted = 0,查不到已经删除的数据。

第二,@NaturalId把mobile标记为自然业务主键。虽然数据库主键还是自增id,但业务上经常要按手机号查会员。用session.byNaturalId(MemberInfo.class).using("mobile", "13800000000").load()就可以按自然键直接命中,而且 Hibernate 内部会缓存这个映射关系,效率并不差。

第三,@Formula让orderCount在每次加载实体时用子查询动态算出来。这个字段不写库,只作为只读属性存在。适合低频统计字段,比如会员详情页面显示订单数。如果这个数字要经常被列表页循环查询,那就要好好评估性能,不能无脑用。

4.3 老教程里的@Type用法在 Hibernate 6 里少碰

网上能看到大量旧教程在实体里这么写:

@Type(type = "org.hibernate.type.StringType") private String remark;

这个写法在 Hibernate 6 上很容易出问题。因为 Hibernate 6 把类型系统重构了一遍,原来五花八门的字符串类型风格被重新整理了。真要是自定义字段映射,在 Hibernate 6 里应该优先考虑@JdbcTypeCode,或者干脆自定义实现UserType,按官方文档来。看到@Type(type = "org.hibernate.type.StringType")这种代码,直接删掉,绝大多数情况下默认映射已经够用了。

这个坑特别典型:一个项目从 Hibernate 5 升级到 6,启动报了一堆UnsupportedTypeException,排查到最后就是旧@Type写法引发的。

5. 事务边界与 Session 生命周期:原生 API 的生死必修课

5.1 openSession 和 getCurrentSession,别选错

Session 有两种获取方式,很多初学 Hibernate 的人分不清,我把它们放在一起对照。

获取方式Session 生命周期事务关系使用场景
sessionFactory.openSession()每次新建,需要手动 close自己手动 begin、commit、rollback批量任务、临时数据处理、非 Spring 环境
sessionFactory.getCurrentSession()绑定当前事务,由 Spring 事务管理器负责关闭跟随 @Transactional 自动提交或回滚业务 Service 方法内 CRUD

在 Spring 环境下,绝大多数情况应该使用getCurrentSession()。它拿到的是和当前事务绑定的同一个 Session,后续第二次、第三次调用拿的都是同一个,不会因为开了新 Session 导致一级缓存失效。

openSession()用手动管理 Session 非常容易出泄漏问题。有一次我在一个批处理服务里图方便,每次循环openSession(),忘了写 finally close,跑了三个小时后连接池直接打满,整个系统报 cannot borrow JDBC connection 卡死。这种惨痛教训希望你们不要重复。

5.2 一个规范业务层写法

Service 代码应该长成这样。

package com.demo.service; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.demo.entity.UserInfo; @Service public class UserInfoService { @Autowired private SessionFactory sessionFactory; @Transactional public Long createUser(String username, String email) { Session session = sessionFactory.getCurrentSession(); UserInfo user = new UserInfo(); user.setUsername(username); user.setEmail(email); session.persist(user); return user.getId(); } @Transactional(readOnly = true) public UserInfo getById(Long id) { Session session = sessionFactory.getCurrentSession(); return session.get(UserInfo.class, id); } @Transactional public void updateEmail(Long id, String newEmail) { Session session = sessionFactory.getCurrentSession(); UserInfo user = session.get(UserInfo.class, id); user.setEmail(newEmail); } }

createUser方法里,session.persist(user)之后我没有手动flush或者commit,因为事务在@Transactional注解声明的方法返回后,Spring 会把事务提交,Hibernate 会自动把持久化上下文里的变化同步到数据库。updateEmail同理,我连update方法都没调用,只是改了实体对象的一个 setter,事务结束时脏检查就会自动发出 UPDATE 语句。这就是 Hibernate 的脏检查机制,在不调saveOrUpdate的情况下照样能自动更新。

5.3 不要在大事务里跑一万条循环

最常见的性能事故,是有人把大批量循环操作包在同一个@Transactional方法里。比如循环一万条用户数据逐个更新,Session 的一级缓存会一直增长,到后来整个应用内存膨胀。

正确做法是分批提交,并定期清理 Session 缓存。

package com.demo.service; import java.util.List; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class BatchProcessService { @Autowired private SessionFactory sessionFactory; public void batchUpdate(List<Long> ids) { int batchSize = 200; for (int i = 0; i < ids.size(); i += batchSize) { List<Long> subIds = ids.subList(i, Math.min(i + batchSize, ids.size())); doUpdate(subIds); } } @Transactional(propagation = Propagation.REQUIRES_NEW) public void doUpdate(List<Long> subIds) { Session session = sessionFactory.getCurrentSession(); for (int i = 0; i < subIds.size(); i++) { UserInfo user = session.get(UserInfo.class, subIds.get(i)); user.setEmail("new-" + i + "@example.com"); if (i % 50 == 0) { session.flush(); session.clear(); } } } }

REQUIRES_NEW让每一批次事务独立提交,即使某一次失败,也不会把前面批次一锅端。flush强制把缓存里的变更同步到数据库,clear清空一级缓存,避免内存持续占用。参数大小要看单批数据量和数据库承受能力,一般 200 到 500 之间的批次比较稳。

6. 实际碰到的几个“非 JPA”高频坑,附带完整排查链路

6.1 No current session context configured

这个问题出现频率最高的场景,是一个方法明明没有加@Transactional,却在方法里直接调了sessionFactory.getCurrentSession()。Spring 环境里getCurrentSession()依赖事务同步器,没有事务时它不知道要给你哪个 session,就会抛HibernateException: No current session context configured。

修复方案有两个。第一个是补上@Transactional:在意读取动作加@Transactional(readOnly = true),在意写操作加@Transactional。第二个是确认hibernate.current_session_context_class没有被改成thread。原生 Hibernate 的 Spring 集成,理论上不需要自己设置这个属性,如果配置文件里顺手写了thread,会直接绕过 Spring 的 Session 绑定逻辑,导致事务管理器拿不到当前 Session。

6.2 实体在事务外被 JSON 序列化,懒加载直接爆掉

这是定时炸弹级别的问题。Controller 层返回一个UserInfo实体,这个实体里有一个@ManyToOne关联属性,例如会员的等级对象。当你事务内查到实体并返回后,Spring MVC 拿 Jackson 做 JSON 序列化时,事务已经关闭了,Hibernate 去加载关联对象时发现 Session 没了,抛LazyInitializationException: could not initialize proxy - no Session。

有些人会想到把spring.jpa.open-in-view设为 true。这个玩意在 Spring Data JPA 项目里默认是 true,但我们的方案根本没走 JPA 自动配置,这个配置项管不到我们。就算以后切回到 JPA,我也建议保持默认关闭,因为 OSIV 会把数据库连接一直绑定到视图渲染结束,高并发下连接池压力山大。

正确的修复方式是:在事务方法内部就把关联数据打好,返回一个 DTO 对象,而不是直接返回实体。比如查询会员详情时,在@Transactional(readOnly = true)方法里把member.getLevel().getLevelName()取值放到一个MemberDetailVO里,事务结束后 DTO 已经是纯内存对象,随便序列化都不会炸。

6.3 多个数据源情况下,方言别让 Hibernate 猜

如果工程里有多个DataSource,hibernate.dialect一定要显式指定。Hibernate 6 会自动探测数据库类型,但在多数据源并存或者某些特殊驱动场景下,自动探测会拿到错误的方言。比如有一个 MySQL 数据源,一个 PostgreSQL 数据源,在同一个应用里切换时,让 Hibernate 猜方言的结果就是 SQL 生成风格一把梭,要么语法错误,要么类型转换异常。

配置方法很简单,刚才的hibernateProperties()里已经有hibernate.dialect了,逐数据源创建LocalSessionFactoryBean时给不同的属性值。老教程里的MySQL5Dialect之类的类名在 Hibernate 6 里已经不存在了,统一用包名不带版本号的org.hibernate.dialect.MySQLDialect,PostgreSQL 用org.hibernate.dialect.PostgreSQLDialect。

6.4 Hibernate 6 的 API 变化,老代码别直接抄

这些年帮人排查过很多 Hibernate 从 5 升 6 的问题,顺手列几个差异点。

老代码的session.saveOrUpdate在 Hibernate 6 里不建议继续当默认 API。新增用persist,更新用merge,语义更接近 JPA 标准,也不踩“明明想 insert 结果执行了 update”的雷。

老代码的session.createCriteria完全不用。Hibernate 6 里 Criteria 查询已经改成 JPA 风格,原生 SQL 想映射实体,用session.createNativeQuery("select * from user_info where id = ?", UserInfo.class)更直接。

老代码的setResultTransformer在 Hibernate 6 已经标废弃。如果你之前靠它把Object[]转成 DTO,现在要么改用addScalar加映射,要么直接标 DTO 构造器。

6.5 启动后表没建出来,先看 hbm2ddl 和作用包

实体扫描没问题、SessionFactory 也打了日志,但数据库里就是没有表。这个问题的排查链路非常固定:先看hibernate.hbm2ddl.auto是不是update或者create,再看setPackagesToScan的包路径和实体所在包是否一致。

如果包路径写成了com.demo.entity但实体类放在com.demo.model,扫描结果必然为空。还有一个我常踩的坑:Hibernate 6 的hbm2ddl.auto=update对新加字段的表,如果字段类型映射和数据库已有的列类型不兼容,不会像 create 那样直接重建,而是会静默跳过,看起来就像“表没建出来”。开发阶段可以直接用create,每次启动都重建表,确认映射没问题后再切update。

我在实际项目里的习惯是,启动时先用CommandLineRunner打印两个东西:实体列表和方言类名。实体列表确认扫描成功,方言类名确认 Hibernate 没猜错库类型。两个日志都对,数据表还不对,那就去数据库里看权限,是不是给连接用户没有CREATE TABLE权限。这个低级问题我在一个测试环境里排查了大半个小时才想起来。

7. 最后补一句我自己的封装习惯

原生 Hibernate 用久了你就会发现,如果不做一层封装,每个 Service 里都会出现“注入 SessionFactory + 调 getCurrentSession”的样板代码。我喜欢的做法是做一个极简泛型 DAO,封装findById、findAll、persist、merge这几个通用操作,业务特定查询还是直接写 HQL 放到各业务 DAO。这套方案的体验接近 Spring Data JPA 的轻量,又保持了对 Hibernate 底层 API 的完全可见性。

如果你只打算做 CRUD,还是那句话,直接用 Spring Data JPA 更香。但如果你确实需要 Hibernate 原生注解能力、需要在旧系统里平稳过渡、要把事务和 Session 完全掌控在手里,非 JPA 这条路线值得一试。配置上最大的门槛就是依赖选择、自动配置排除、LocalSessionFactoryBean 声明,这三步走通,后面基本就是实体映射和事务管理的常规操作了。

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

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

立即咨询