
SpringBoot 项目里做持久层绝大多数教程上来就是 spring-boot-starter-data-jpa甚至很多人已经忘了 Hibernate 原生 API 长什么样。但真正接手过老项目、或者对持久层有强控制需求的人一定遇到过这类场景代码里全是 Session、Query、Criteria根本没法也不用迁到 Spring Data JPA又或者只想在 SpringBoot 里把 Hibernate 当一套独立的 ORM 工具用完全不想被 JPA 规范那层“壳”约束。这篇文章要解决的就是这种事SpringBoot 和 Hibernate 直接整合用注解完成实体映射但刻意不碰 Spring Data JPA、不碰 EntityManager 那套标准接口而是走 Hibernate 自己的 SessionFactory Session HQL。这种整合方式最早流行于 SSH 时代后来 SpringBoot 流行后似乎被 JPA 风格压过了但在很多存量系统迁移、二次开发、以及需要细粒度控制 SQL 生成的场景里它依然是最稳的选择。下面我会从依赖搭建、SessionFactory 配置、注解映射、事务管理到高频踩坑把完整链路走一遍适合已经在用 SpringBoot、但对 Hibernate 还是“会用但不深究”的开发者参考。1. 为什么放着 JPA 不用非要碰原生 Hibernate1.1 JPA 与 Hibernate 的关系辨析先说清楚几层概念。JPA 是 Java 持久化 API 规范定义了一套实体注解、EntityManager 接口、JPQL 语句标准Hibernate 是该规范最主流的实现。而 Spring Data JPA 是 Spring 在 JPA 之上再做的一层仓储抽象帮你自动生成 DAO 实现。很多项目所谓的“SpringBoot 整合 Hibernate”实际是引入了 starter-data-jpa底层用 Hibernate但开发时用的是 Repository 接口和 EntityManager真正 Hibernate 独有的能力很少直接暴露。拿生活类比JPA 就像统一的插座标准Hibernate 是生产插座的厂商Spring Data JPA 则是装修公司直接按标准给你装好面板。大多数场景面板够用但如果要插座厂商的特殊功能比如定时断电保护、异常检测装修公司反而不一定给你留开关。Hibernate 原生整合就是绕开这层装修直接去触摸插座厂商的控制面板。这里还需要澄清一个容易混淆的点Hibernate 原生注解配置底层用的仍然是 JPA 规范里的注解比如 Entity、Id、Column 来自 jakarta.persistence 包真正 Hibernate 自己的注解如 DynamicUpdate、BatchSize、Fetch 属于 org.hibernate.annotations 包。“非 JPA”指的是不用 Spring Data JPA 和 EntityManager 这一套编程模型而不是说完全脱离 JPA 注解标准。Hibernate 这种“基于 JPA 注解但走原生 Session 编程模型”的组合是它多年兼容策略的结果。1.2 “非 JPA”整合的真实适用场景不是所有项目都适合绕开 JPA 套件。如果是全新项目、核心需求是快速 CRUD、团队又熟悉 Spring Data直接用 JPA 风格显然省事。原生 Hibernate 整合主要出现在下面几类场景。一是存量 SSH 系统迁移到 SpringBoot。这类老代码通常大量使用 sessionFactory.getCurrentSession()、session.createQuery(from User where ...)甚至 Hibernate 5 时代的 Criteria API。迁移阶段最怕的不是功能移植而是把整个持久层换一套模型所以保留 Hibernate 原生编程接口能大幅降低风险。我手上就有个从 SSM Hibernate 迁到 SpringBoot 的项目数据访问层几乎原封不动搬过来只改了几处 Spring 配置。二是需要深度使用 Hibernate 专属能力。二级缓存、事件监听、自定义拦截器、Envers 审计这些能力在 Hibernate 原生 API 下取用最直接。走 JPA 标准接口虽然也能触达部分能力但经常要做强转或配置穿透不如原生环境来得坦荡。三是团队对持久层控制欲望较强的情况。比如想让 SQL 生成完全可控关掉一些自动优化自己拼 HQL/Criteria。Spring Data JPA 的动态代理在复杂查询场景下容易让人有失控感原生 Session 编程模型更符合“自己动手”的手感。2. 环境准备与依赖管理避开 starter-data-jpa 的暗雷2.1 版本配套怎么选SpringBoot 自身版本直接决定 Hibernate 的大版本线。这里有个特别关键的差异Spring Boot 2.x 和 3.x 中Hibernate 实体注解的包名不一样。技术栈组合Hibernate 版本实体注解包名Spring ORM 整合类Spring Boot 2.xHibernate 5.6javax.persistence.*org.springframework.orm.hibernate5.LocalSessionFactoryBeanSpring Boot 3.xHibernate 6.xjakarta.persistence.*org.springframework.orm.hibernate6.LocalSessionFactoryBeanSpring Boot 3.x 全面转向 Jakarta EEHibernate 6 也跟进把实体注解从 javax 迁到 jakarta。如果你照着老教程复制代码很容易出现编译期找不到 jakarta.persistence 包或者运行时版本冲突这个细节会在后面单独展开。以当前主流环境 Spring Boot 3.x Hibernate 6.x 为基线下文大部分配置和代码都基于这套组合。项目若是 Spring Boot 2.x只需要注意“javax 换成 jakarta 的反向替换”和整合类包名差异其余思路完全一致。2.2 Maven 依赖清单与自动配置排除先给一份可直接用的 pom 片段。这里最关键的一点是不要引入 spring-boot-starter-data-jpa。很多人已经形成了思维惯性以为整合 Hibernate 就必须通过 Spring Data其实完全绕开它是很干净的做法。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework/groupId artifactIdspring-orm/artifactId /dependency dependency groupIdorg.hibernate.orm/groupId artifactIdhibernate-core/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency版本号不需要手写SpringBoot 的依赖管理已经把 hibernate-core 和 spring-orm 的版本管好了。hibernate-core 的坐标在不同版本线略有差异Spring Boot 2.x 的依赖管理也能覆盖所以父工程用 spring-boot-starter-parent 即可。这里有个真实存在的问题如果项目里某个第三方依赖传递引入了 spring-data-jpa你会发现即使没显式写 starter-data-jpaHibernateJpaConfiguration 也可能自动生效。此时可以用依赖排除把不需要的传递依赖摘掉比如在排除段里剔除 spring-data-jpa 和 hibernate-core。dependency groupId某依赖groupId/groupId artifactId某依赖artifactId/artifactId exclusions exclusion groupIdorg.springframework.data/groupId artifactIdspring-data-jpa/artifactId /exclusion exclusion groupIdorg.hibernate.orm/groupId artifactIdhibernate-core/artifactId /exclusion /exclusions /dependency如果排除不掉或者某些原因仍然引入 starter-data-jpa那需要在启动类上显式排除 JPA 自动配置SpringBootApplication(exclude { org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.class, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration.class }) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }但注意这个排除写法是“不得已而为之”。SpringBoot 自动配置是条件装配不引入相关类就不会生效。正常情况下你把 starter-data-jpa 从依赖里拿掉HibernateJpaConfiguration 因为 classpath 条件不满足根本不会装配不需要额外排除。最干净的状态是项目里压根没有 spring-data-jpa 依赖。2.3 javax 还是 jakarta一个会让旧教程直接翻车的包名差异这一段必须单独讲。Spring Boot 3.x Hibernate 6 的实体注解包名是 jakarta.persistence.而 Spring Boot 2.x Hibernate 5 用的是 javax.persistence.。很多从旧项目复制来的实体类一放进新项目就报一堆 cannot find symbol基本都是在编译期导入错了包。在 pom 里写好依赖后用 mvn dependency:tree 检查一下实际引入的 hibernate-core 版本。如果确认是 6.x实体类里就应该写import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table;那么 Hibernate 自己的注解呢org.hibernate.annotations 这个包名不含 javax/jakarta 前缀所以像 DynamicUpdate、BatchSize 这些注解在 Hibernate 5 和 6 里都能用反而省心。一个很容易被忽略的坑是如果项目里同时存在 Hibernate 5 和 6 的依赖比如某个老 jar 里带了 hibernate-core 5Spring Boot 3 的依赖仲裁可能被覆盖启动时出现多个 SessionFactory 创建失败这属于依赖冲突后面排查章节再展开。3. 构建 SessionFactory非 JPA 整合的核心配置3.1 基于 LocalSessionFactoryBean 建立会话工厂在 Spring 容器里创建 Hibernate 的 SessionFactory推荐方式是使用 spring-orm 提供的 LocalSessionFactoryBean。这个类不是 Hibernate 自己提供的而是 Spring 对 SessionFactory 的封装负责把 DataSource 注入、实体扫描、Hibernate 属性设置串起来。一个完整的配置类长这样import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.hibernate6.HibernateTransactionManager; import org.springframework.orm.hibernate6.LocalSessionFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.Properties; Configuration public class HibernateConfig { Bean public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean factoryBean new LocalSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setPackagesToScan(com.example.demo.domain); 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.naming.physical-strategy, org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy); factoryBean.setHibernateProperties(props); return factoryBean; } Bean public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) { return new HibernateTransactionManager(sessionFactory); } }再配合 application.properties 里配置数据源spring.datasource.urljdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyour_password spring.datasource.driver-class-namecom.mysql.cj.jdbc.DriverDataSource 由 SpringBoot 的 DataSourceAutoConfiguration 自动创建默认是 HikariCP。LocalSessionFactoryBean 拿到 DataSource 后会在这里获取 JDBC 连接所以不需要额外配置连接池参数。setPackagesToScan 的作用是扫描指定包下所有带 Entity 注解的类逐一注册到 SessionFactory 的实体映射中。这个扫描范围不要设置太宽我见过有人直接把整个项目包路径丢进去启动时扫描了一堆无关类虽然不影响功能但会拖慢启动速度。按分层习惯扫描 domain、entity、po 这类实体包即可。3.2 核心配置项逐一拆解Hibernate 配置项很多但不是每个都需要调。我列几个实际项目中最常碰到的配置项作用我的建议hibernate.dialect方言告诉 Hibernate 当前数据库类型用于生成合适的 SQL开发环境建议显式配置避免启动时自动检测的日志噪音生产环境必须确认与实际库版本匹配hibernate.show_sql控制台打印所有 HQL 生成的 SQL开发环境开 true生产环境关闭或者改为 true 但配合日志级别过滤hibernate.format_sql美化打印的 SQL方便阅读配合 show_sql 使用建议开 truehibernate.hbm2ddl.auto是否根据实体映射自动建表/更新表结构开发用 update生产环境建议 validate 或 nonehibernate.naming.physical-strategy实体属性到数据库列名的命名策略Hibernate 6 默认就是驼峰转下划线跨团队协作统一用这个比较好hibernate.jdbc.batch_size批量 JDBC 操作的每批大小大批量插入/更新时调到 20~50常规 CRUD 不用管dialect 需要注意版本匹配。Hibernate 6 的 MySQLDialect 默认面向 MySQL 8如果你用的还是 MySQL 5.7在分页、锁这类 SQL 上会有差异。Oracle、SQL Server、PostgreSQL 这类数据库差异更大dialect 配置一定要选对。不要相信网上那种“方言可以不配Hibernate 会自动检测”的说法自动检测在大部分情况下可靠但在某些数据库版本识别不了时会退化而且会额外产生一次元数据查询。写死的成本很低收益很实在。hbm2ddl.auto 是另一个容易翻车的地方。开发阶段设为 update 方便迭代但如果有同事在本地把表结构改乱了代码一启动可能触发结构变更隐患很大。生产环境我见过不止一次把 update 带到生产某个实体字段修改直接导致线上表被重建事故虽然 Hibernate 的 update 不会删列但改动大宽表结构前依然可能锁表。生产库风险最低的做法是 none 数据库变更脚本走正规迁移流程至少也要用 validate 防止代码和表结构漂移。3.3 事务管理器选型为什么不用 JpaTransactionManager这段是很容易被忽略却很重要的配置。很多人在 SpringBoot 里写事务习惯了默认注入 JpaTransactionManager但非 JPA 整合的环境下事务管理器应该用 HibernateTransactionManager。HibernateTransactionManager 和 DataSourceTransactionManager 有一定血缘关系它本身继承自 DataSourceTransactionManager但在其基础上增加了 Hibernate Session 的绑定和同步。具体表现是事务开启期间getCurrentSession() 能拿到同一个 Session事务提交后 Session 会自动关闭事务回滚时Hibernate 的脏检查和数据同步会正确配合。Bean public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) { return new HibernateTransactionManager(sessionFactory); }如果事务管理器配的是 DataSourceTransactionManager也不是完全不行但 Hibernate 的 Session 不会自动和当前事务绑定你调 sessionFactory.getCurrentSession() 大概率会收到 No CurrentSessionContext configured 的报错或者不得不在每处手工 openSession/closeSession。这样的代码很快就会失控。此外统一用 Spring 的事务注解做边界控制而不是在 DAO 里写 session.beginTransaction()。Spring 声明式事务才能让事务管理可配置、可切面化尽量让事务边界停留在 Service 层而非数据访问层。4. 注解映射实体从基础到关联关系4.1 一个完整的实体类示例Hibernate 原生注解配置的实体类和 JPA 风格非常接近因为大部分注解本身就是 JPA 的。你先看一个基础的 User 表映射import jakarta.persistence.*; Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name nick_name, nullable false, length 50) private String nickName; Column(nullable false) private Integer age; Transient private String remark; Version Column(name version) private Integer version; ManyToOne(fetch FetchType.LAZY) JoinColumn(name dept_id) private Department department; // getter / setter / 无参构造 }几个需要注意的点Column 里的 name 和物理列名映射。上面例子 nickName 属性对应 nick_name 列其实即使不写 Column(name nick_name)在 Hibernate 6 的默认驼峰转下划线策略下也会自动映射过去。这里显式写出来是因为老库的表结构很可能和历史字段命名不一致显式声明最保险也方便后期 DBA 核对。Transient 标记的字段不参与任何持久化既不会建列也不会插入/更新。常用于放一些临时计算值、页面展示用的辅助字段。实体类必须有无参构造方法。如果你重载了带参构造一定要补一个无参构造出来Hibernate 反射创建实例时默认走的是无参构造。这个问题在 Lombok 时代尤其容易踩AllArgsConstructor 配 Data 但忘了加 NoArgsConstructor运行时直接抛 InstantiationException。4.2 关联关系映射的关键细节关联映射是 Hibernate 注解配置里最容易翻车的地方尤其是懒加载和外键归属。常见场景用户属于部门部门下有多个用户。部门实体的一端Entity Table(name t_department) public class Department { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name dept_name) private String deptName; OneToMany(mappedBy department, fetch FetchType.LAZY) private ListUser users new ArrayList(); }用户实体已经在上面的例子里写了 ManyToOne JoinColumn。这里最关键的一点是 mappedBydepartment它告诉 Hibernate这段关系由 User 端的 department 属性维护外键在 t_user 表上。如果你在两端都写上 JoinColumnHibernate 会认为这是两个独立的外键生成冗余的关联信息甚至产生多余的更新列。从 Hibernate 实际操作角度看OneToMany 建议显式加 fetch FetchType.LAZY原因看关联集合是否必需。比如查询部门列表时通常不需要把部门下所有用户全查出来懒加载能减少大量无效 SQL。但懒加载引来的经典问题 LazyInitializationException我放到常见问题章节细说。ManyToOne 在 JPA 规范里默认是 EAGER 的也就是查询 User 时会默认把部门一并查出来。如果你不想被这种隐式 join 带偏强烈建议显式设置 fetch FetchType.LAZY。需要部门数据的场景用 HQL join fetch 按需加载效果更好。4.3 DynamicUpdate 与 Version容易被忽略的执行期优化注解先说 Version。它是乐观锁控制的标准注解Hibernate 在更新时会自动把 version 带上 where 条件然后 version1。如果更新影响行数为 0说明数据可能被并发修改过会抛出乐观锁异常。用法很简单Version Column(name version) private Integer version;只要字段类型是 int、long、short 或 java.sql.Timestamp 都可以Hibernate 会自动维护值。开发时注意一点如果前端不小心把 version 字段传回来了或者插入了非法初始值会导致首次插入就出现 version 不为 0 的情况所以建议 version 字段不要暴露给前端或者至少在 insert 时强制赋初值 0。DAO 层建议用 session.save() 而不是手工拼接 INSERT。再来看 DynamicUpdate。默认情况下 Hibernate 更新一条记录会把所有列写入 UPDATE 语句哪怕只有一个字段发生了变化。大宽表场景这是很浪费的而 DynamicUpdate 能让 Hibernate 只更新发生变化的字段Entity Table(name t_user) org.hibernate.annotations.DynamicUpdate public class User { // 常规字段... }这类动态 SQL 有个取舍它确实减少了网络传输和数据库工作量但动态生成的 SQL 变多Hibernate 的 SQL 缓存命中率会下降。对于字段很多、更新频率高的表收益明显对于字段只有三五个的小表反而没必要。同理 SelectBeforeUpdate 表示更新前先查询一遍当前值再决定是否更新适合希望减少无意义 UPDATE 请求的场景但会引入额外 SELECT。两个注解都不是默认开启要按表逐个判断。DynamicUpdate 在 Hibernate 5、6 中都位于 org.hibernate.annotations 包是 Hibernate 自有的注解。走 Spring Data JPA 时虽然也能用但会经常遇到代理对象的奇怪行为原生 Session 环境下这种行为反而是可控的。这恰好是“非 JPA”整合的价值之一Hibernate 独有特性用起来不必绕路。5. 数据访问层封装与事务边界控制5.1 基于 Session 的 DAO 层写法数据访问层可以直接操作 Session。使用 getCurrentSession() 而不是 openSession()是因为在线程绑定事务的环境下Session 和事务是绑定的提交后自动关闭不需要手动 close。一套完整的 DAO 大概长这样Repository public class UserDao { private final SessionFactory sessionFactory; public UserDao(SessionFactory sessionFactory) { this.sessionFactory sessionFactory; } public User findById(Long id) { return sessionFactory.getCurrentSession().get(User.class, id); } public Long save(User user) { return (Long) sessionFactory.getCurrentSession().save(user); } public void update(User user) { sessionFactory.getCurrentSession().update(user); } public void delete(User user) { sessionFactory.getCurrentSession().delete(user); } public ListUser findByAgeGreaterThan(Integer age) { return sessionFactory.getCurrentSession() .createQuery(from User u where u.age :age, User.class) .setParameter(age, age) .getResultList(); } }save 方法返回类型可能让人疑惑。Hibernate 5 的 save 返回 SerializableHibernate 6 中返回类型变成了泛型其实 Hibernate 6 中 save 方法仍然返回实体 ID你可以按 Long 接收但更推荐用 persist 语义加 flush 后读取实体 ID避免类型转换的别扭。如果你只是想插入一条记录更干净的写法是public void persist(User user) { sessionFactory.getCurrentSession().persist(user); }区别在于 save 在事务中立即生成 insert 还是延迟执行会有差异persist 更符合 JPA 语义。原生整合里两个都可以用但团队要统一否则有人用 save 有人用 persist排查问题时容易产生混乱。DAO 查询时注意 HQL 操作的是实体名和属性名不是表名和列名。比如 User 实体对应的表是 t_user但写 HQL 时用 UsernickName 属性虽然列名为 nick_name但 HQL 中写 nickName。这个区别新手最容易踩因为 SQL 能跑通HQL 一跑就报语法错误。5.2 事务边界控制在 Service 层别在 DAO 层开事务一个常见反模式每个 DAO 方法里面自己 beginTransaction自己 commit。这样看起来每个操作都“成功提交”了但一旦一个业务操作需要调用三次 DAO中间任何一步失败无法整体回滚。正确做法是只给 Service 层方法加 TransactionalDAO 层保持无事务状态Service public class UserService { private final UserDao userDao; private final DepartmentDao departmentDao; public UserService(UserDao userDao, DepartmentDao departmentDao) { this.userDao userDao; this.departmentDao departmentDao; } Transactional(rollbackFor Exception.class) public void transferUserToDepartment(Long userId, Long deptId) { User user userDao.findById(userId); Department dept departmentDao.findById(deptId); user.setDepartment(dept); userDao.update(user); } }我见过不少人会在 Transactional 上纠结 rollbackFor Exception.class 的写法。Spring 默认只对 RuntimeException 回滚如果业务代码抛的是受检异常不写 rollbackFor 是不会触发回滚的。所以在涉及文件操作、外部调用、或者自定义受检异常的业务方法上建议显式加上 rollbackFor Exception.class统一思维事务方法内所有异常都回滚别留下“某个步骤失败但数据已经改了”的状态。另外要注意 Transactional 是基于 Spring AOP 代理的同类内部方法调用不经过代理事务注解不会生效也就是经典的“自调用失效”。原生 Hibernate 整合没有绕开这个机制所以别在一个类内部通过 this 调用另一个带 Transactional 的方法想要事务生效必须从外部进入。6. 实战中容易踩的坑与排查思路6.1 LazyInitializationException为什么查询成功了访问集合却报错这是 Hibernate 使用者遇到频率最高的问题没有之一。触发场景通常长这样Service 事务方法内查了一个 Department 对象事务边界结束、Session 关闭然后在 Service 外层比如 Controller 层去访问 dept.getUsers()。此时 user 集合处于懒加载状态但 Session 已经不可用Hibernate 只能抛出 LazyInitializationException。这个问题本质是“Session 生命周期短于数据使用生命周期”。解法大致有三种但业界口碑差异很大。第一种把事务边界上移或者在事务内完成所有关联数据的加载。比如需要返回部门及其用户列表就在事务方法内直接查询并装配成 DTO/VO。这种方法最稳也是我最推荐的做法。数据访问状态在事务内明确不会留隐患。第二种HQL 中用 join fetch 一次性把关联集合查出来。from Department d left join fetch d.users where d.id :id这样 Hibernate 在查询时通过一条 join SQL 把 users 集合填充好即使 Session 关闭集合也已经在内存中。注意 join fetch 和分页同时使用容易产生重复结果和内存分页问题要小心处理。第三种注册 OpenSessionInViewFilter延长 Session 到整个请求结束。Spring 的 spring-orm 支持 OpenSessionInViewInterceptor 或 Filter很多老项目为了图省事会直接开启。但我不建议默认使用。Session 生命周期拉长后一个简单请求也会在响应渲染完毕前一直占用 JDBC 连接高并发场景下数据库连接池很容易被打满而且这种“事务外读数据”的方式会让调试难度直线上升。如果你在维护老项目看到 OpenSessionInView 已经开启也别急着摘掉先评估团队习惯和接口响应结构再逐步切到事务内装配方案。6.2 N1 查询日志里 SQL 数量多得吓人N1 查询在关联映射场景下极其常见。典型场景查询 10 个部门然后逐一访问每个部门的 user 集合Hibernate 先查一次部门列表再为每个部门单独发一条 user 查询总共 11 条 SQL。数据量一涨数据库轻松被打挂。排查思路很简单开发环境把 show_sql 和 format_sql 打开跑一遍典型查询数一下 SQL 条数。如果发现类似“先 1 条查询再 N 条同结构查询”的序列基本就是 N1。解法一个是前面说的 HQL join fetch把关联集合在一条查询里带出来。另一个是 BatchSizeOneToMany(mappedBy department, fetch FetchType.LAZY) org.hibernate.annotations.BatchSize(size 20) private ListUser users new ArrayList();加了这个注解后Hibernate 在懒加载集合时会在一次查询里用 IN 条件批量加载最多 20 个部门下的用户SQL 数量从 N 条降到个位数。代价是单条 SQL 的 IN 条件可能较长但整体数据库压力小很多。还有个容易被忽略的地方BatchSize 只对懒加载的关联集合或实体生效对 HQL 查询本身无效。如果你已经明确知道一个查询需要关联数据优先用 join fetch 或设计良好的 DTO 查询别指望 BatchSize 兜底。6.3 Hibernate 6 迁移与几个容易翻车的细节Hibernate 6 对老代码的破坏性主要是包名和 API 调整。常见的会有这么几类第一javax.persistence 全部换成 jakarta.persistence。这个在前面提过不再赘述。真遇到大量实体类要改的时候全局替换包名导入即可但改完一定要检查有没有遗漏的 import否则编译期就报错。IDEA 的全项目替换功能可以节省大量时间但要小心替换范围把业务代码里的字符串也扫进去。第二Hibernate 5 时代的 session.createCriteria() 已经被移除。老代码里如果写这种 Criteria API在 Hibernate 6 编译期就能发现。现在应该使用 JPA Criteria 或者直接写 HQL// Hibernate 5 老写法已失效 // return session.createCriteria(User.class).add(Restrictions.eq(age, age)).list(); // Hibernate 6 推荐写法 return session.createQuery(from User u where u.age :age, User.class) .setParameter(age, age) .getResultList();第三查询 API 的 list() 和 uniqueResult() 在新版本中很多被标记废弃建议统一用 getResultList() 和 getSingleResult()。开发时不注意升级后编译不会报错但会有一堆 warning维护起来不痛快干脆一开始就用新 API。第四Hibernate 6 的 MySQLDialect 封装的是 MySQL 8 的语法。如果项目使用 MySQL 5.7尽量实测分页、锁和时区相关的 SQL必要时在 dialect 选择上做单独配置。Hibernate 会尽量用新语法但老版本数据库不一定支持全部特性。还有一个容易忽略的Spring Boot 3 项目即使你没写 spring.jpa 前缀配置但如果类路径有 hibernate-core 和某个 JPA 实现SpringBoot 可能自动创建一些 JPA 相关 bean。检查日志里有没有类似 JpaBaseConfiguration 的自动配置生效确有必要时把 DataSource 和 SessionFactory 的日志打开看一眼。7. 个人实际操作的一点建议这套非 JPA 整合的方案我已经在不同项目里反复用了几轮整体感受是好处在可控性强坏处在样板代码要自己写。Spring Data JPA 把所有仓库统一交给了动态代理省事但调试时要翻一层原生 Hibernate 整合则把数据访问的逻辑全部摊在明面上遇到问题能直接看到 SQL 和 Session 的状态。如果让我给一个明确的使用边界新项目、团队缺少 Hibernate 深度经验、业务以常规 CRUD 为主我仍然建议用 Spring Data JPA没必要为了“原生”而原生。但如果是老项目迁移、大量 Hibernate 遗留代码、或者明确要用二级缓存、Envers 审计、事件拦截这类深度功能那这套整合方式就是最合理的选择。最后分享一个小技巧开发环境把 show_sql 和 format_sql 打开不只是为了看 SQL也要注意分析 Hibernate 为你生成的 update 语句到底更新了哪些列。很多你以为没修改的数据实际被 Hibernate 整个对象快照比对后可能还是生成了一条完整 UPDATE。遇到这种问题先查实体类是不是忘了加 DynamicUpdate再排查是不是对了不该动的关联对象比如 setDepartment 时把整个部门对象赋值上去导致关联外键也被视为变更。对这些细节的把控才是原生 Hibernate 整合真正考验经验的地方。