说实话,每次有人在群里问“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 值得用的,是下面这几个表里列出的注解。它们单独任何一个拿出来,都能少写几十行业务代码。
| 注解 | 包 | 典型使用场景 | 注意事项 |
|---|---|---|---|
@Where | org.hibernate.annotations | 软删除,查询时自动附带 deleted=0 条件 | 对关联集合也能生效,但复杂 HQL join 下容易引起迷惑 |
@Formula | org.hibernate.annotations | 实体里加一个只读的统计字段,例如订单数 | 对应于 SQL 片段,不走缓存,频繁查询要评估性能 |
@SQLDelete | org.hibernate.annotations | 调用 delete 时改成逻辑删除 UPDATE | 通常和@Where搭配使用 |
@NaturalId | org.hibernate.annotations | 业务唯一键,例如手机号 | 可以用session.byNaturalId()直接加载 |
@DynamicUpdate | org.hibernate.annotations | 更新时只生成变化字段的 UPDATE 语句 | 有少量动态 SQL 拼装开销,不适合超高频简单更新 |
@DynamicInsert | org.hibernate.annotations | 插入时只对有值的字段生成 INSERT | Hibernate 6 里语义变化要小心 |
@CreationTimestamp/@UpdateTimestamp | org.hibernate.annotations | 创建时间、更新时间自动填入 | 不要和数据库默认值混淆 |
@Fetch | org.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 声明,这三步走通,后面基本就是实体映射和事务管理的常规操作了。