刚开始接触 Spring Boot 数据层的时候很多人都会有这样一个困惑JPA 和 Hibernate 到底是不是同一个东西为什么有的教程装一个spring-boot-starter-data-jpa就说“用上了 JPA”有的代码里却到处写着 Hibernate 的Session事实上这两个名字代表的是规范和实现的关系而“JPA Hibernate 双实现”是指在一个项目里同时发挥两者的优势用标准 JPA 的接口保持代码整洁又借用 Hibernate 原生 API 去搞定批处理、过滤、二级缓存这些高级场景。我们项目里的订单服务就是这么改的整套思路比较典型写出来给准备入门 Spring Boot 数据层的同学参考。1. 概念拆解JPA 与 Hibernate 到底在解决什么问题1.1 JPA 是接口标准Hibernate 是解决方案先用一个生活化的比喻JPA 就好像餐厅门口贴的菜单规定了有哪些菜、怎么点、上菜的流程Hibernate 则是后厨那位真正掌勺的厨师负责把食材加工成菜品。客人点菜的时候只要看菜单不需要关心后厨是怎么开火的。对应到技术里JPAJava Persistence API是 Java 官方提出的一套持久层规范只定义接口、注解和行为约定例如Entity、Id、EntityManager这些都是 JPA 的而 Hibernate 是这套规范的具体实现还额外提供了一批规范之外的能力比如Session、Criteria、Filter、二级缓存整合等等。Spring Boot 默认选的实现就是 Hibernate。你只引入spring-boot-starter-data-jpa底层依赖里其实已经带上hibernate-coreSpring Data JPA 做的事情则是进一步封装EntityManager让你不需要写persist、find这些底层方法直接继承一个JpaRepository接口就能获得save、findById、deleteById等能力。这层套一层的设计带来了不少便利也造成了不少误解。我见过不少同事把“Spring Data JPA”和“Hibernate”当成同义词使用排查问题时容易找错方向。1.2 项目里为什么需要“双实现”既然 Spring Data JPA 已经够用为什么还要刻意去搞一套 Hibernate 原生实现最直接的原因是标准接口覆盖了高频操作但覆盖不了所有细节。举个例子。我们做订单系统时有一次需求要在凌晨把前一天所有超时未支付的订单批量改成“已取消”。如果直接写一个循环去调repository.save()上万条数据跑起来会很慢因为每一条都要走一次完整的 ORM 生命周期生成 SQL、执行 update、刷新持久化上下文。此时更合理的做法是用 Hibernate 的批处理机制开启 JDBC 批处理、设置每批条数、手动 flush 和 clear这样能明显减少数据库交互次数。可是这些 API 在 Spring Data JPA 的JpaRepository里是看不到的必须通过 Hibernate 的Session才能施展。另一个场景是动态过滤。某些查询需要根据当前登录用户、当前租户、数据权限自动拼条件。用 JPA 写Query通常要在每个方法上重复写条件维护成本很高而 Hibernate 的Filter可以定义在实体上运行时由 Session 统一开启过滤条件代码会干净很多。这就是“双实现”存在的意义项目主路径用标准 JPA 保底遇到性能洞或者复杂场景再通过 Hibernate 原生接口补齐。2. 先把双实现的项目地基搭好2.1 引入依赖厘清 starter 之间的关系新建一个 Spring Boot 项目时核心依赖就一个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency有人可能会好奇要不要单独再加一个hibernate-core依赖在绝大多数情况下不需要。spring-boot-starter-data-jpa是 Spring Boot 提供的集成模块它把spring-data-jpa、hibernate-core、hibernate-entitymanager等组件一起带进来并完成自动配置。你在pom.xml里单独写 Hibernate 版本反而可能和 Spring Boot 管理的版本冲突除非你真的需要覆盖默认版本解决某个 Bug。我们项目里曾经为了让 Hibernate 支持某个数据库方言临时指定过更高版本结果引入了hibernate-validator的版本冲突花了不少时间去对齐。所以我的建议是能用 boot 管理的版本就不要自己指定除非有明确的兼容性理由。数据库驱动方面如果用的是 MySQL 8.x依赖是dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果是其他数据库替换为对应的驱动即可。再配合 Lombok 减少实体类样板代码我一般也会顺手加上dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency最容易被忽略的是 web 依赖。如果你的服务是接口服务记得引入spring-boot-starter-web否则 Controller 那层无法启动如果只是跑一个数据处理任务可以不引入。2.2 配好数据源和 Hibernate 关键属性配置文件application.yml里除了常规的数据源配置有几个和 Hibernate 密切相关的属性值得细看spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true order_inserts: true order_updates: true jdbc: batch_size: 50ddl-auto是新手最需要警惕的配置。开发阶段用update能自动建表、加字段确实方便但生产环境一旦误用可能导致表结构被意外改动。我们团队的上线规范里生产环境明确要求必须是validate或none表结构全部交给专门的迁移脚本来管理。这个坑建议你提前踩一遍早改早安心。show-sql打开后控制台会打印执行的 SQL适合学习阶段为了阅读方便再配上format_sql。生产环境建议关闭避免日志噪音。order_inserts和order_updates分别对应批量插入、批量更新时的排序优化能让 Hibernate 把相同类型的操作合并到一起。batch_size则是告诉 Hibernate 每次往数据库发送的批次大小配合上面提到的rewriteBatchedStatementstrue才能真正让 MySQL 走批量写入路径否则你可能看到代码执行了很多条 SQL 却没有提升性能。3. 双实现落地一边用 JpaRepository一边用 Hibernate Session3.1 JPA 标准路径实体 Repository Service先看实体。我们的订单实体非常简单为了演示双实现就保留几个关键字段Data Entity Table(name t_order) public class OrderEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 32) private String orderNo; private BigDecimal amount; private Integer status; }这里Entity和Id都是 JPA 规范的注解Hibernate 负责执行。Table指定物理表名避免默认生成的表名和业务命名不一致GenerationType.IDENTITY对应 MySQL 自增主键。然后定义仓库接口。Spring Data JPA 会根据方法名自动生成实现也可以在Query里手动写 JPQLpublic interface OrderRepository extends JpaRepositoryOrderEntity, Long { ListOrderEntity findByStatus(Integer status); Query(select o from OrderEntity o where o.orderNo like :keyword) ListOrderEntity search(Param(keyword) String keyword); }Service 层就是标准的 CRUDService public class OrderJpaService { private final OrderRepository orderRepository; public OrderJpaService(OrderRepository orderRepository) { this.orderRepository orderRepository; } Transactional public OrderEntity create(OrderEntity order) { return orderRepository.save(order); } Transactional(readOnly true) public ListOrderEntity listByStatus(Integer status) { return orderRepository.findByStatus(status); } }这一段就是绝大多数业务代码在用的“JPA 实现”。好处是简洁、可读性好、移植性强因为写的是规范层面的接口哪天底层实现从 Hibernate 换成别的 JPA 实现业务代码基本不用动。当然实际项目中这种替换很少发生这种可移植性更多是一种心理上的安全感。3.2 Hibernate 原生路径通过 EntityManager 获取 SessionHibernate 原生 API 的核心是Session。在 Spring Boot 项目里拿到 Session 有一个非常干净的方式注入 JPA 的EntityManager再调用unwrap(Session.class)。这么做既没有脱离 Spring 的事务管理又能直接使用 Hibernate 的独有方法。我们封装一个专门的 DAO 来演示Repository public class OrderHibernateRepository { PersistenceContext private EntityManager entityManager; Transactional public void batchUpdateStatus(ListLong ids, Integer newStatus) { Session session entityManager.unwrap(Session.class); // 使用 Hibernate 原生 HQL 执行批量更新 org.hibernate.query.QueryOrderEntity query session.createQuery(update OrderEntity o set o.status :status where o.id in :ids, OrderEntity.class); query.setParameter(status, newStatus); query.setParameter(ids, ids); query.executeUpdate(); // 清空持久化上下文确保后续查询读到最新数据 session.clear(); } }注意这里不能用返回泛型为OrderEntity的 HQL 去执行update实际上 Hibernate 的更新语句返回的是影响行数应该用int updated query.executeUpdate();泛型可以省略。我在早期写这段代码时踩过坑更新语句硬是声明成QueryOrderEntity运行时直接报TransactionException后来才发现 Hibernate 对 DML 语句的返回类型有严格限制。再展示一个批量插入的典型写法Transactional public void batchInsert(ListOrderEntity orders) { Session session entityManager.unwrap(Session.class); session.setJdbcBatchSize(30); int i 0; for (OrderEntity order : orders) { session.persist(order); if (i % 30 0) { // 手动触发 SQL 发送并断开对象关联避免持久化上下文无限膨胀 session.flush(); session.clear(); } } }这段代码和JpaRepository.save()的循环插入最大的区别就是手动控制了flush时机和批次大小并且在每批结束后清理持久化上下文。如果不清理当你有 1 万条订单要插入时Hibernate 的一级缓存会保存 1 万个实体对象不仅占用内存最后的flush还可能因为关联关系导致 SQL 执行顺序混乱。这个“分批 flush clear”的节奏是 Hibernate 批量操作的精髓。3.3 两种实现如何分工协作在我们项目里并不是把所有代码都改成了 Hibernate 原生写法而是严格分工普通 CRUD、分页查询、按条件简单查询全部走JpaRepository代码量少、维护容易。批量状态更新、大数据量写入、需要在查询时自动附加数据权限条件、需要精细控制缓存刷新的场景走 Hibernate 原生接口。有一个很关键的点这两套实现操作的是同一套实体类、同一个数据库表因为底层都是同一个SessionFactory。所以在一个事务里先通过 JPA 仓库保存数据再通过 Hibernate 原生接口做更新是完全可行的。Spring Boot 自动配置把EntityManagerFactory和SessionFactory关联起来了EntityManager.unwrap(Session.class)拿到的就是当前事务对应的那个 Session。看到这里你应该能明白为什么标题叫“双实现”而不是“两套独立实现”了——它们共享运行时资源只是对外暴露不同的 API 层次。4. 两种方式的优缺点和核心差异4.1 JPA 与 Hibernate 原生 API 对比表格对比项JPA 标准方式Spring Data JPAHibernate 原生 API代码风格接口声明式简单直观命令式控制粒度细动态条件查询方法名规则或 Query需要逐个定义Criteria 或 Filter运行时动态拼接更灵活批量插入/更新默认性能不佳支持 JDBC 批处理、手动 flush/clear内置缓存控制通过属性配置层级较少可直接操作 Session、二级缓存 API多租户/数据权限过滤需要自己去 AOP 或 SQL 拼接Filter 在实体层统一处理可移植性高换了 JPA 实现也能跑低绑定 Hibernate但基本不考虑迁移上手门槛低适合大多数场景高一点需要理解 Session 生命周期这张表基本能回答“我到底该学哪个”的疑问。实际上不应该有“要么用 JPA 要么用 Hibernate”的对立思维而是把 JPA 当作普遍适用的操作入口把 Hibernate 当作性能与扩展能力的工具箱。4.2 Hibernate 高阶能力Filter 和 StatelessSession既然聊到双实现有两个 Hibernate 原生能力值得单独说明。第一个是Filter。它允许在实体上百宝箱式地定义过滤条件运行期才决定是否启用。比如订单实体上有逻辑删除字段deleted以往每个查询都要写where deleted 0很容易漏。用 Hibernate 这样处理Data Entity Table(name t_order) FilterDef(name notDeleted, parameters ParamDef(name deleted, type Boolean.class)) Filter(name notDeleted, condition deleted :deleted) public class OrderEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Boolean deleted; }在需要过滤的查询方法里Session session entityManager.unwrap(Session.class); session.enableFilter(notDeleted).setParameter(deleted, false); ListOrderEntity orders session.createQuery(select o from OrderEntity o, OrderEntity.class) .list(); session.disableFilter(notDeleted);这样在 Service 层不需要每个方法里手动拼条件过滤规则和实体绑定在一起语义清晰。这也是 JPA 标准接口无法直接做到的效果之一。第二个是StatelessSession适合纯查询、纯导出的场景。它不维护一级缓存不管理实体生命周期性能开销更低适合一次性读取大量数据做转换。用起来大概是这样try (StatelessSession statelessSession sessionFactory.openStatelessSession()) { statelessSession.createQuery(select o from OrderEntity o, OrderEntity.class) .setCacheable(false) .list(); }注意无状态会话没有脏检查修改了实体对象后 Hibernate 不会自动更新数据库必须显式调用update方法。批量导出报表时用它很舒服但别指望它帮你做复杂级联操作会把你绕晕。5. 实战中踩过的坑和排查经验5.1 事务边界夹住了 Session 的生命周期使用EntityManager.unwrap(Session.class)时最需要小心的是事务边界。如果方法上加了Transactional那么 Session 会绑定到当前事务事务提交时 Session 自动关闭。如果方法没有加事务而你直接调用session.saveOrUpdate()Hibernate 会自己维护 Session 生命周期但 Spring 的事务边界和 Session 的打开关闭时间可能不一致容易出现LazyInitializationException或“No Session”异常。我们的突发状况是这样的有个实时库存接口没有加Transactional里面先通过HibernateRepository查了一次订单然后在 Controller 层试图访问订单里的商品列表结果报LazyInitializationException。排查到最后发现查询方法是在无事务状态下执行的返回的实体对象持有的是已经关闭的 Session。修复方案很朴实要么在 Service 方法上加Transactional(readOnly true)要么在实体关联字段上显式配置fetch FetchType.EAGER。但用Transactional(readOnly true)更通用不会因为过度使用强制加载把关联数据无脑查出来。5.2 批量更新不生效多半是 flush 时机乱掉我在第一次写 3.2 节的批量更新代码时遇到过一个诡异的现象代码执行了executeUpdate()返回的受影响行数是 0但数据库里数据确实变了。后来理清原因Hibernate 在执行 DML 语句之前会自动检查持久化上下文里是否有同类型的待更新实体。如果在这种更新之前用 JPA 仓库改了同一批订单的状态Hibernate 可能会先 flush 这些实体的变化接着executeUpdate()执行 HQL 更新两个操作互相影响日志里看到的行数自然对不上。解决方案是在执行 HQL 批量更新前先决定好要不要flush()和clear()。如果是纯批量更新就在方法最开始session.clear()保证持久化上下文干净别让旧实体状态干扰。如果先有插入操作就手动session.flush()再session.clear()把顺序理顺。5.3 一级缓存带来的“查不到最新数据”这个问题也让我们折腾过。背景是这样批量更新完成后代码调用了session.clear()。但如果某个方法漏掉了clear()紧接着又用 JPA 仓库查询同一主键的订单会因为一级缓存返回旧状态的对象导致页面显示的还是更新前的状态。这个问题说出来简单真正排查却比较费劲因为session.clear()清的是持久化上下文而 Spring Data JPA 的查询在同一个事务里默认也走同一级缓存。后来我们总结经验凡是涉及批量更新的代码要么在更新后立即clear()要么在查询方法上明确使用Modifying(clearAutomatically true)。如果你用的是 JPA 的Query更新语句可以把注解配置成Modifying(clearAutomatically true, flushAutomatically true) Query(update OrderEntity o set o.status :status where o.id :id) int updateStatus(...)clearAutomatically在更新完自动清理持久化上下文flushAutomatically在更新前自动 flush两个开关一起加上能省掉很多“数据看起来没变”的排查时间。5.4 多数据源场景下必须区分 SessionFactory 和 EntityManagerFactory如果项目里接入多个数据源双实现就更容易出乱子。一个数据源对应一个EntityManagerFactory也就对应一个SessionFactory。当你执行entityManager.unwrap(Session.class)时拿到的 Session 一定来自当前EntityManager绑定的那个工厂。如果注入方式不对或者PersistenceContext没有指定unitName很可能操作的是默认数据源导致你看着在写库A的数据实际上跑到库B上去了。处理多数据源时通常要在配置里明确声明两个EntityManagerFactory并使用PersistenceContext(unitName xxx)区分。这样确实麻烦不少所以如果业务允许我建议优先考虑让双实现局限在单数据源场景里先确保链路稳定再扩展。6. 从“会写代码”到“理解设计”我的实践心得6.1 “双实现”不是让你两头都写一遍初学者容易陷入一个理解偏差既然 JPA 和 Hibernate 都能操作数据库那是不是要写两套 Service一套用 JPA一套用 Hibernate然后让用户切换完全不是。真正的双实现是让两级 API 合理分工底层共享同一套连接池、事务和实体映射。我见过一些项目把 Hibernate 原生写法硬塞进所有代码里理由是“直接操作 API 更透明”。结果如何代码变长了事务控制容易出错而且业务代码里到处是Session、Transaction团队协作时学习成本直线上升。反过来也有项目只因某一次批量性能问题就全面逃离 JPA跳进了 MyBatis 的怀抱。我的观点是与其整个换框架不如先搞清楚你遇到的性能瓶颈是不是真的需要换方案。大多数情况只是没用对批量模式调整 Hibernate 配置就解决了。6.2 学习路径建议逐个突破如果你正在学 Spring Boot 的数据层我建议先按这个顺序来第一遍只学 Spring Data JPA把实体映射、Repository 方法名派生、Query、分页排序用熟知道一个流程完整的业务模块是怎么跑起来的。第二遍再深入 Hibernate 原生 API重点搞懂Session、Transaction、持久化上下文、缓存机制这几个概念不要急着写高级用法先把unwrap的流程跑通。第三遍开始做性能优化类题目比如批量插入 1 万条数据、复杂动态查询、多租户过滤这时候再把 Filter、Batch、StatelessSession 拿出来实战。这样学下来你会慢慢发现JPA 和 Hibernate 不是可替换的关系而是分层的关系。JPA 帮你守住开发效率Hibernate 帮你守住系统下限。6.3 最后分享一个排查技巧如果你在工作里遇到“JPA 的方法执行慢但 Hibernate 又查不出原因”的情况我强烈建议先打开 SQL 日志再把hibernate.generate_statistics属性打开spring: jpa: properties: hibernate: generate_statistics: true启动后 Hibernate 会在日志里打印实体加载数量、查询次数、SQL 执行次数等统计信息。有一次我就是靠这些统计发现某张表的关联查询产生了 N1 问题而不是某个 SQL 本身慢。对于一个“JPA Hibernate 双实现”的项目理解这些运行期统计指标比死记硬背 API 重要得多。工具是死的数据是活的对着统计数据去调优比对着文档猜要靠谱很多。
阅读完成 · 觉得有帮助?