JPA 规范与 Hibernate 入门详解
定位:从 JPA 规范认知到基础 CRUD 与主键策略的入门全解
适用版本:Hibernate ORM 6.x(JDK 17+、Jakarta Persistence 3.1)、Spring Boot 3.x
目录
一、框架认知
1.1 历史演进
Hibernate 的诞生源于对 EJB 2.x 实体 Bean 的反抗。2001 年,Gavin King 在开发企业项目时无法忍受 EJB 实体 Bean 的笨重与低效,于是编写了一个把普通 Java 对象(POJO)映射到关系表的框架------这就是 Hibernate 1。
| 时间 | 里程碑 | 意义 |
|---|---|---|
| 2001 | Hibernate 1 发布 | POJO 持久化,终结 EJB 实体 Bean 时代 |
| 2005 | Hibernate 3.x | 注解支持(Hibernate Annotations),成为 Java ORM 事实标准 |
| 2006 | JPA 1.0(JSR-220) | 吸收 Hibernate 注解形成规范,Gavin King 任规范负责人 |
| 2009 | JPA 2.0 + Hibernate 3.5 | Criteria API、元素集合、二级缓存标准化 |
| 2012 | Hibernate 4.x | Service 层重构、Integrator 扩展点、多租户 |
| 2015 | Hibernate 5.x | 代理库从 Javassist 迁移到 Byte Buddy |
| 2022 | Hibernate 6.x | Jakarta 化、SQM 语义查询模型、新类型系统、SelectionQuery API |
理解这段历史有两个实用价值:其一,明白「JPA 源于 Hibernate」,所以 JPA 注解是 Hibernate 注解的标准化子集;其二,明白 5.x 到 6.x 是破坏性重构而非平滑升级,这解释了 Boot 2 升 Boot 3 时持久层的迁移成本。
1.2 定位:全自动 ORM
ORM(Object-Relational Mapping)框架按自动化程度分为两类:
┌──────────────────────────────────────────────────────────────┐
│ 半自动 ORM(以 MyBatis 为代表) │
│ 开发者:写 SQL + 定义映射 框架:执行 SQL、封装结果 │
│ 控制权:SQL 完全由人掌控 │
│ │
│ 全自动 ORM(以 Hibernate/JPA 为代表) │
│ 开发者:定义实体模型 + 操作对象图 框架:生成 SQL、同步状态 │
│ 控制权:SQL 由框架按映射与状态自动生成 │
└──────────────────────────────────────────────────────────────┘
全自动意味着:你操作的是对象图(order.getCustomer().getName()),框架负责把它翻译成 JOIN、把对象状态变化翻译成 UPDATE、把重复读取转化为缓存命中。收益是开发效率与领域模型表达能力,代价是失去对每条 SQL 的直接控制------这也是 Hibernate 争议的来源。
1.3 JPA 与 Hibernate 的关系
一句话:JPA 是规范,Hibernate 是实现。
| 维度 | JPA(Jakarta Persistence) | Hibernate ORM |
|---|---|---|
| 本质 | API 规范(接口 + 注解 + JPQL 语法标准) | 规范的最流行实现 |
| 包名 | jakarta.persistence.* |
org.hibernate.* |
| 查询语言 | JPQL | JPQL + HQL 扩展 |
| 能力边界 | 标准定义的部分 | 标准超集(原生 SQL 改进、过滤器、多租户等) |
| 其他实现 | EclipseLink(参考实现)、OpenJPA | --- |
工程实践中推荐的态度:默认面向 JPA 编程(注解、EntityManager、JPQL),把 Hibernate 专有特性当作「可选增强」,并在代码中集中封装,降低将来换实现的迁移成本。
1.4 同类产品对比
| 框架 | 自动化程度 | SQL 控制 | 学习曲线 | 数据库无关性 | 典型场景 |
|---|---|---|---|---|---|
| Hibernate/JPA | 全自动 | 低(可原生 SQL 逃逸) | 陡 | 强 | 领域模型复杂、CRUD 密集 |
| MyBatis | 半自动 | 完全 | 平缓 | 弱 | SQL 复杂、需 DBA 介入 |
| Spring Data JDBC | 半自动(聚合根) | 低 | 平缓 | 中 | 简单聚合、无懒加载需求 |
| jOOQ | SQL DSL | 完全(类型安全) | 中 | 中 | 复杂查询 + 类型安全 |
选型判据:
- 领域模型复杂度:有继承、多对多、聚合图等复杂模型 → JPA;模型就是几张平表 → MyBatis/JDBC 足够。
- CRUD 占比:标准增删改查占 80% 以上 → JPA + Spring Data 收益最大。
- SQL 优化诉求:报表、复杂统计、DBA 强管控 → MyBatis 或 jOOQ。
- 团队技能:JPA 的隐式行为(脏检查、懒加载、级联)需要团队理解持久化上下文,否则坑多。
生产中最常见的是共存模式:核心领域模型走 JPA,复杂报表查询走 MyBatis 或 JdbcTemplate,两者共用同一数据源与事务管理器。
1.5 生态版图
| 构件 | 作用 |
|---|---|
hibernate-core |
核心:映射、持久化上下文、查询、缓存 |
hibernate-jpamodelgen |
编译期注解处理器,生成静态元模型类(User_),供类型安全 Criteria |
hibernate-envers |
实体版本化审计,自动记录每次变更到审计表 |
hibernate-spatial |
GIS 空间类型与函数(Point、Polygon) |
| 协同组件 | Bean Validation(校验集成)、HikariCP(连接池)、Flyway/Liquibase(模式演进) |
二、JPA 规范体系
2.1 规范演进时间线
| 版本 | 年份 | 关键特性 |
|---|---|---|
| JPA 1.0(JSR-220) | 2006 | 注解映射、JPQL、EntityManager |
| JPA 2.0(JSR-317) | 2009 | Criteria API、@ElementCollection、共享缓存、悲观锁 |
| JPA 2.1(JSR-338) | 2013 | 存储过程调用、EntityGraph、AttributeConverter、@Convert |
| JPA 2.2 | 2017 | Java 8 时间类型原生支持、getResultStream() 流式查询 |
| Jakarta Persistence 3.0 | 2020 | 命名空间 javax.persistence → jakarta.persistence(EE 移交 Eclipse 基金会) |
| Jakarta Persistence 3.1 | 2022 | @GeneratedValue 支持泛型 ID、UUID 生成、增强日期时间支持 |
对应关系要记牢:JPA 2.2 ↔ Hibernate 5.x ↔ Spring Boot 2.x ;Jakarta Persistence 3.1 ↔ Hibernate 6.x ↔ Spring Boot 3.x。
2.2 三大核心支柱
JPA 规范由三部分构成:
- ORM 映射元数据 :用注解(
@Entity、@OneToMany...)或orm.xml描述对象与表的映射。注解是绝对主流。 - 运行时 API :
EntityManagerFactory、EntityManager、EntityTransaction,负责实体的生命周期管理。 - JPQL :面向对象的查询语言。
select o from Order o where o.customer.name = :name------操作的是实体与属性路径,由实现翻译为方言 SQL。
2.3 javax → jakarta 迁移
Spring Boot 2 升级到 Boot 3 时,持久层最直接的改动就是包名:
java
// Boot 2.x(Hibernate 5.x)
import javax.persistence.Entity;
import javax.persistence.Id;
// Boot 3.x(Hibernate 6.x)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
迁移检查清单:
- 全局替换
javax.persistence→jakarta.persistence(注意javax.transaction→jakarta.transaction)。 - 三方库版本对齐:QueryDSL 5.1+(jakarta classifier)、Spring Data JPA 3.x、Hibernate Envers 6.x。
- 检查 Hibernate 专有注解使用点(
org.hibernate.annotations.*在 6.x 中有删除/改名,如@Type体系重写)。
2.4 可移植性边界
| 内容 | 可移植性 |
|---|---|
jakarta.persistence 标准注解 |
可跨实现 |
| JPQL 标准语法 | 可跨实现 |
@org.hibernate.annotations.Filter/@Formula 等 |
绑定 Hibernate |
HQL 特有语法(如 elements()、隐式组件路径扩展) |
绑定 Hibernate |
hibernate.dialect 原生 SQL |
绑定具体数据库 |
工程惯例:标准能力随便用;Hibernate 扩展统一封装在仓储层个别方法中并注释说明原因,避免散落在业务代码里。
三、快速入门
3.1 最小环境
方式一:原生 JPA(persistence.xml)
xml
<!-- META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
<persistence-unit name="demo-pu" transaction-type="RESOURCE_LOCAL">
<class>com.example.domain.User</class>
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="123456"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
java
EntityManagerFactory emf = Persistence.createEntityManagerFactory("demo-pu");
方式二:Spring Boot(推荐,生产主流)
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo
username: root
password: 123456
jpa:
hibernate:
ddl-auto: none # 生产禁用 update/create,交给 Flyway
show-sql: false # 生产关闭,改用日志框架控制
properties:
hibernate:
format_sql: true
Boot 下无需 persistence.xml:自动配置扫描 @Entity 实体,构建 LocalContainerEntityManagerFactoryBean,方言通过 JDBC 元数据自动探测。
3.2 核心 API 生命周期
┌────────────────────────────────────────────────────────┐
│ EntityManagerFactory(重量级、线程安全、应用单例) │
│ │ 持有:映射元数据、二级缓存、连接池配置 │
│ ├── createEntityManager() │
│ │ │ │
│ │ ▼ │
│ │ EntityManager(轻量、非线程安全、短生命周期) │
│ │ │ 持有:持久化上下文(一级缓存) │
│ │ ├── getTransaction() → EntityTransaction │
│ │ ├── persist / find / merge / remove │
│ │ └── close() │
│ └── close()(应用关闭时) │
└────────────────────────────────────────────────────────┘
原生 API 的标准使用模板:
java
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
User user = new User();
user.setName("alice");
em.persist(user);
tx.commit(); // 提交时 flush,执行 INSERT
} catch (RuntimeException e) {
tx.rollback();
throw e;
} finally {
em.close(); // 必须关闭,释放一级缓存与连接
}
Spring 环境下以上模板全部由容器接管:注入 EntityManager(实为事务绑定的共享代理)或直接使用 Spring Data 仓储,事务由 @Transactional 控制。
3.3 第一个实体
java
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
private LocalDateTime createdAt;
protected User() { } // JPA 要求:无参构造(可为 protected)
public User(String name) {
this.name = name;
this.createdAt = LocalDateTime.now();
}
// getter/setter 省略
}
实体类的硬性与软性要求:
| 要求 | 级别 | 原因 |
|---|---|---|
| 无参构造 | 硬性 | 反射实例化(Constructor.newInstance) |
@Id 主键 |
硬性 | 身份映射的依据 |
| 非 final 类/方法 | 强烈建议 | 懒加载代理需要继承与覆写 |
实现 Serializable |
建议 | 二级缓存序列化、分布式会话 |
重写 equals/hashCode |
建议 | 基于业务键或 ID,勿用全字段(详见 03 篇) |
这些约束都来自框架机制:反射实例化要求无参构造,字节码代理要求非 final,身份映射要求主键。理解机制就不会死记硬背。
3.4 使用纪律
- EMF 单例 :构建昂贵(解析全部映射、初始化缓存),应用启动时创建一次,关闭时
close()。 - EM 短生命周期 :典型作用域是一次请求或一个事务。它是非线程安全的,绝不能作为静态字段或单例 Bean 成员共享。
- 写操作必须在事务内 :无事务调用
persist会抛TransactionRequiredException。 - 只读查询也建议显式只读事务 :
@Transactional(readOnly = true)可以让 Hibernate 跳过脏检查、让数据库走只读优化。
四、基础 CRUD 模板
4.1 persist:插入
java
tx.begin();
User user = new User("alice");
em.persist(user); // 瞬态 → 持久态;此刻未必执行 SQL
tx.commit(); // flush 时生成并执行 INSERT
关键点:
persist只是登记「这个对象要插入」,实际 INSERT 延迟到 flush。但若主键是IDENTITY,必须立即执行 INSERT 才能拿到自增值,所以会立即发 SQL。persist之后对象进入持久态,user.getId()在 flush 后即可读取(IDENTITY 立即有值;SEQUENCE 在取号后即有值)。- 若对象已设置 ID 且库中已存在对应行,
persist会抛EntityExistsException------它不承担「有则更新」职责。
4.2 find:按主键查询
java
User user = em.find(User.class, 1L); // 返回托管实体或 null
- 先查持久化上下文(一级缓存):同一事务内已加载过则直接返回同一实例 (
==为 true)。 getReference(User.class, 1L)返回代理,访问属性时才真正查库;行不存在时首次访问抛EntityNotFoundException。Hibernate 6.0 起getReference语义与 JPA 对齐,仅用于关联赋值占位场景(避免多余 SELECT)。
4.3 merge:合并更新
java
tx.begin();
User detached = getUserFromSomewhere(); // 游离对象(有 ID,不在上下文中)
User managed = em.merge(detached); // 返回托管副本
managed.setName("bob");
tx.commit(); // 脏检查 → UPDATE
高频误区:merge 的入参不会被托管,返回值才是托管实例。继续操作入参对象等于操作一个「快照」,修改不会同步到数据库。
4.4 remove:删除
java
tx.begin();
User user = em.find(User.class, 1L); // 必须是托管对象
em.remove(user); // 进入删除态
tx.commit(); // flush 时执行 DELETE
- 不能直接
remove一个瞬态或游离对象(传游离对象会抛IllegalArgumentException)。 - 若存在外键引用该行,删除时抛
ConstraintViolationException------需要级联删除或先解除引用。
4.5 辅助操作一览
| 方法 | 作用 | 典型场景 |
|---|---|---|
refresh(entity) |
用数据库当前值覆盖内存状态 | 撤销内存修改、读最新值 |
detach(entity) |
移出持久化上下文 → 游离 | 提前释放大对象,防一级缓存膨胀 |
contains(entity) |
判断是否托管 | 调试与条件分支 |
clear() |
清空整个上下文 | 批量任务分批提交(见 08 篇) |
flush() |
手动同步挂起变更到数据库 | 需要在同事务内先写后查 |
4.6 Spring Data 版本的 CRUD
同样的操作在 Spring Data JPA 中收敛为 JpaRepository 方法,底层语义不变:
| Spring Data 方法 | 对应原生语义 |
|---|---|
save(entity) |
无 ID 或 ID 为空 → persist;有 ID → merge |
findById(id) |
find |
delete(entity) |
内部先确保托管再 remove |
getReferenceById(id) |
getReference |
注意 save 的「有 ID 走 merge」约定:这是 Spring Data 对 isNew() 的默认判断,自定义主键赋值(如雪花 ID)时必须重写 isNew() 或使用 Persistable,否则新插入会变成先查再更新。这个坑在 07 篇展开。
五、主键生成策略
5.1 GenerationType 全景
java
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 数据库自增列
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "seq_user",
allocationSize = 50)
private Long id;
| 策略 | 机制 | 数据库要求 | 批处理友好 | 评价 |
|---|---|---|---|---|
AUTO |
交给实现决定 | --- | --- | Hibernate 6 下倾向选择序列/池化序列,行为不直观,不建议依赖 |
IDENTITY |
数据库自增列 | MySQL、SQL Server | 否 | MySQL 唯一实用的数据库生成方案 |
SEQUENCE |
数据库序列对象 | PostgreSQL、Oracle | 是 | 写入吞吐最优 |
TABLE |
独立表模拟序列 | 任意 | 否(加锁) | 性能最差,仅理论可移植 |
UUID(@Uuid) |
应用侧生成 UUID | 任意 | 是 | 无序导致 B+ 树页分裂,适合 36 字节字符串或哈希分布场景 |
5.2 SEQUENCE vs IDENTITY:为什么影响批处理
这是本章最重要的原理点,也是高频面试题:
IDENTITY 的写入时序:
INSERT → 数据库生成自增值 → JDBC getGeneratedKeys() 取回
∵ persist 后框架需要立即把主键填入实体(身份映射依赖主键)
∴ 每条 INSERT 必须单独执行,JDBC batch 被禁用
SEQUENCE 的写入时序:
nextval(seq) 取一段号(如 50 个)→ 内存分配给实体 → 批量 INSERT
∵ 主键在 INSERT 之前就已确定
∴ 可以配合 jdbc.batch_size 批量提交
实测影响:万级批量插入场景,SEQUENCE + batch 相比 IDENTITY 逐条执行可以有 3~10 倍 的吞吐差距。因此:数据库支持序列时(PostgreSQL、Oracle)一律选 SEQUENCE;MySQL 只能选 IDENTITY 或应用侧雪花算法。
5.3 @SequenceGenerator 的 allocationSize 陷阱
allocationSize(默认 50)表示一次从序列取多少号。它必须与数据库序列的 INCREMENT BY 一致,否则:
- 建库脚本
CREATE SEQUENCE seq_user INCREMENT BY 1,而实体声明allocationSize = 50; - Hibernate 假设每次取号后序列前进了 50,实际只前进了 1;
- 结果:两个应用实例/两次启动取到重叠号段 → 主键冲突。
生产规范:迁移脚本中显式声明 INCREMENT BY 50,并在代码评审中核对两者一致。
5.4 分布式场景:应用侧赋值
微服务/分库分表场景常放弃数据库生成,改为应用侧雪花算法:
java
@Id // 注意:不加 @GeneratedValue
private Long id;
public Order() {
this.id = SnowflakeIdGenerator.nextId();
}
优点:无数据库依赖、趋势递增对 B+ 树索引友好、跨库全局唯一。注意点:时钟回拨处理、发号器部署唯一性,属于分布式 ID 话题,详见数据库知识库相关文档。
六、总结
- JPA 是规范,Hibernate 是实现 。面向
jakarta.persistence标准编程保证可移植,Hibernate 扩展特性集中封装。 - 全自动 ORM 的交换:开发者操作对象图,框架负责 SQL 生成与状态同步;收益是效率与模型表达力,代价是隐式行为(需要理解持久化上下文,见 03 篇)。
- API 生命周期纪律:EMF 应用单例、EM 短生命周期非线程安全、写操作必须事务包裹。
- CRUD 四件套语义 :
persist登记插入、find一级缓存优先、merge返回托管副本(入参不被托管)、remove仅删托管对象。 - 主键策略 :优先
SEQUENCE(批处理友好),MySQL 用IDENTITY或应用侧雪花;allocationSize必须与序列增量一致。 - 版本对应:Boot 3.x ↔ Hibernate 6.x ↔ jakarta 包名,升级时全局替换并核对三方库版本。
七、常见高频面试题
1. JPA 和 Hibernate 是什么关系?为什么要分规范和实现?
要点:JPA(Jakarta Persistence)是持久化规范,定义注解、EntityManager API 和 JPQL;Hibernate 是其最流行的实现,并提供超集能力。分层意义:面向规范编程可跨实现迁移(如换 EclipseLink),避免厂商锁定;实现厂商在规范之上竞争特性。
2. Hibernate 和 MyBatis 的区别?如何选型?
要点:Hibernate 全自动(对象图操作、SQL 自动生成、脏检查、缓存、懒加载),MyBatis 半自动(SQL 手写、映射托管)。选型:领域模型复杂、CRUD 密集、需要数据库无关性 → Hibernate;SQL 复杂多变、需要 DBA 精细调优 → MyBatis。两者可共存:领域模型走 JPA,报表走 MyBatis。
3. EntityManager 和 EntityManagerFactory 的区别与使用纪律?
要点:EMF 重量级(持有映射元数据、二级缓存),线程安全,应用级单例;EM 轻量(持有一个持久化上下文),非线程安全,请求/事务级用完即关。写操作必须事务包裹,否则抛 TransactionRequiredException。
4. persist 和 merge 的区别?
要点:persist 把瞬态对象登记为插入(对象本身被托管),主键回填后不可再改身份;merge 把游离/瞬态对象的状态复制到托管实例并返回该托管实例,入参本身不被托管。带 ID 的新对象误用 persist 会抛 EntityExistsException;合并后继续操作入参是最常见错误。
5. find 和 getReference 的区别?
要点:find 立即查库(一级缓存优先),不存在返回 null;getReference 返回代理,首次访问属性才查库,不存在抛 EntityNotFoundException。getReference 适合「只需要引用做关联赋值、不访问属性」的场景,可省一次 SELECT。
6. IDENTITY 和 SEQUENCE 主键策略的区别?为什么 IDENTITY 不利于批量插入?
要点:IDENTITY 依赖数据库自增列,INSERT 后通过 getGeneratedKeys 取回,因此每条必须立即单独执行,JDBC 批处理被禁用;SEQUENCE 先取号段再 INSERT,主键在插入前确定,可配合 batch_size 批量提交,万级批量场景吞吐差距可达数倍。MySQL 无序列,只能 IDENTITY 或应用侧雪花。
7. @SequenceGenerator 的 allocationSize 有什么作用?配错会怎样?
要点:表示一次从序列预取的号段大小(默认 50),减少序列争用。必须与数据库序列 INCREMENT BY 一致;不一致时框架按 allocationSize 推算已消耗号段,实际序列只按 INCREMENT 前进,导致多实例取号重叠、主键冲突。
8. Spring Boot 2 升级到 3,持久层要做哪些改动?
要点:包名从 javax.persistence 全局替换为 jakarta.persistence;Hibernate 5.x 升 6.x(破坏性重构:类型系统、部分专有注解变化);Spring Data JPA 升到 3.x;三方库(QueryDSL、Envers 等)换成 jakarta 兼容版本;方言 6.x 起多由 JDBC 元数据自动探测。
9. hbm2ddl.auto 有哪些取值?生产环境怎么用?
要点:none(默认,不处理)、validate(校验表结构与映射一致,不一致启动失败)、update(自动增改表结构,可能丢约束且不可逆)、create(启动删表重建)、create-drop(关闭时删表)。生产一律 none 或 validate,表结构演进交给 Flyway/Liquibase;update 只允许在本地开发使用。
10. 实体类有哪些硬性要求?为什么?
要点:必须有无参构造(反射实例化)、必须有 @Id(身份映射依据);强烈建议非 final(字节码代理需要继承)、实现 Serializable(缓存序列化)、基于业务键重写 equals/hashCode。这些约束都源于框架机制:反射、代理、身份映射。
