Hibernate核心概念
Hibernate 是全自动 ORM 框架,更强调实体对象和数据库表之间的映射。开发者更多操作对象,Hibernate 负责生成 SQL 和维护实体状态。
本页是核心概念摘要。完整的 find -> 持久化上下文 -> 快照 -> 脏检查 -> flush -> SQL -> commit 过程见 Hibernate/JPA 核心全过程原理。
核心流程
flowchart TD
A[操作Entity对象] --> B[Session管理状态]
B --> C[脏检查]
C --> D[生成SQL]
D --> E[JDBC执行]
E --> F[同步数据库]核心概念
| 概念 | 说明 |
|---|---|
| Entity | 实体类,对应数据库表 |
| Session | 持久化上下文 |
| SessionFactory | 创建 Session |
| Transaction | 事务 |
| HQL | 面向对象查询语言 |
| Dirty Checking | 脏检查,自动发现对象变化 |
实体状态
Hibernate 实体常见状态:
stateDiagram-v2
[*] --> Transient
Transient --> Persistent: save/persist
Persistent --> Detached: session关闭
Detached --> Persistent: merge/update
Persistent --> Removed: deleteTransient
瞬时状态,对象刚 new 出来,还没有被 Session 管理。
Persistent
持久化状态,对象被 Session 管理。字段变化可能在事务提交时自动同步到数据库。
Detached
游离状态,对象曾经被持久化,但当前不再被 Session 管理。
Removed
删除状态,事务提交时会删除对应数据库记录。
脏检查
脏检查是 Hibernate 的重要能力。持久化状态下的实体被修改后,Hibernate 会在 flush 时比较快照,发现变化后生成 update SQL。
MyBatis和Hibernate对比
| 对比项 | MyBatis | Hibernate |
|---|---|---|
| SQL控制 | 开发者手写SQL | 框架自动生成SQL |
| 学习重点 | SQL、映射、动态SQL | 实体状态、关联关系、缓存 |
| 灵活性 | 高 | 中 |
| 复杂查询 | 更可控 | 需要优化HQL或原生SQL |
| 适合场景 | SQL复杂、性能可控 | CRUD较多、领域模型清晰 |
使用建议
- 理解实体状态再使用 Hibernate。
- 复杂查询要关注生成的 SQL。
- 避免随意配置双向关联导致查询失控。
- N+1 查询要通过 fetch join、批量抓取等方式处理。
- 对性能敏感场景要打开 SQL 日志观察实际执行。
Hibernate 和 JPA 的关系
JPA 是规范,Hibernate 是实现。可以这样理解:
flowchart TD
A["JPA 规范"] --> B["EntityManager"]
A --> C["Entity 注解"]
A --> D["JPQL"]
A --> E["持久化上下文概念"]
F["Hibernate 实现"] --> G["Session"]
F --> H["脏检查实现"]
F --> I["SQL 生成"]
F --> J["懒加载代理"]
F --> K["缓存和批量优化"]平时在 Spring Data JPA 中写的是 Repository 和 JPA 注解,但实际运行时很多行为由 Hibernate 完成,例如实体代理、脏检查、SQL 生成、懒加载、N+1、flush 时机。
Session / Persistence Context 是什么
Hibernate 的 Session 可以理解为持久化上下文的具体实现,它在一次事务中管理实体对象。
flowchart TD
A["Session 打开"] --> B["查询或 persist 实体"]
B --> C["实体进入 Persistence Context"]
C --> D["保存实体快照"]
D --> E["业务修改实体"]
E --> F["flush 脏检查"]
F --> G["生成 SQL"]
G --> H["事务提交或回滚"]Session 中通常有:
- 托管实体实例。
- 实体快照。
- 待执行的插入、更新、删除动作。
- 一级缓存。
- 懒加载代理需要的上下文。
脏检查完整过程
脏检查不是每次 setter 都立刻发 SQL,而是在 flush 时比较当前状态和快照。
flowchart TD
A["加载实体"] --> B["保存 loadedState 快照"]
B --> C["业务代码修改字段"]
C --> D["flush 触发"]
D --> E["遍历托管实体"]
E --> F["比较当前值和快照"]
F --> G{"是否变化"}
G -- "是" --> H["生成 update SQL"]
G -- "否" --> I["不更新"]这解释了两个现象:
- 修改托管实体不需要显式
update。 - 如果实体不在 Session 中,修改不会自动同步。
flush 什么时候发生
常见触发时机:
- 事务提交前。
- 手动调用
flush()。 - 某些查询执行前,为保证查询能看到当前事务内变更,可能先 flush。
flowchart TD
A["修改托管实体"] --> B{"触发 flush?"}
B -- "commit 前" --> C["脏检查并发 SQL"]
B -- "查询前" --> C
B -- "手动 flush" --> C
C --> D["SQL 发到数据库"]
D --> E{"事务最终结果"}
E -- "commit" --> F["提交"]
E -- "rollback" --> G["回滚 SQL 影响"]flush 后 SQL 已经发出,但事务未必提交,所以仍可回滚。
懒加载代理
Hibernate 为懒加载关联创建代理对象。访问代理属性时,如果 Session 还开着,就发 SQL 加载;如果 Session 已关闭,就报懒加载异常。
flowchart TD
A["查询 Asset"] --> B["department 字段是代理"]
B --> C{"访问 department.name 时 Session 是否存在"}
C -- "存在" --> D["发送 SQL 加载 Department"]
C -- "不存在" --> E["LazyInitializationException"]为什么不建议 Controller 返回 Entity?
- JSON 序列化可能访问懒加载字段。
- 双向关联可能循环引用。
- 暴露数据库字段。
- 序列化过程可能偷偷发 SQL。
N+1 详细示例
实体:
@Entity
public class Asset {
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}代码:
List<Asset> assets = assetRepository.findByStatus(1);
for (Asset asset : assets) {
System.out.println(asset.getDepartment().getDeptName());
}SQL 可能变成:
select * from asset where status = 1;
select * from department where id = ?;
select * from department where id = ?;
select * from department where id = ?;
...解决方式一:fetch join。
@Query("""
select a
from Asset a
join fetch a.department
where a.status = :status
""")
List<Asset> findWithDepartment(@Param("status") Integer status);解决方式二:DTO 投影。
@Query("""
select new com.demo.AssetListDTO(a.id, a.assetCode, d.deptName)
from Asset a
join a.department d
where a.status = :status
""")
List<AssetListDTO> findList(@Param("status") Integer status);列表页更推荐 DTO 投影,因为它只查接口需要的字段。
save、persist、merge 怎么理解
| 操作 | 作用 | 风险 |
|---|---|---|
persist | 把临时态实体变成托管态,准备插入 | 只能用于新实体 |
merge | 把游离对象状态合并到托管对象 | 可能覆盖字段 |
remove | 标记删除 | 事务提交时删除 |
Spring Data save | 新实体 persist,已有实体 merge | 要理解新旧判断 |
merge 的典型坑:
Asset asset = new Asset();
asset.setId(1L);
asset.setAssetName("new-name");
entityManager.merge(asset);如果其他字段都是 null,可能造成不希望的覆盖。更稳妥做法是先查托管实体,再改指定字段。
批量操作为什么容易慢
Hibernate 管理实体状态。如果循环插入 10 万条,持久化上下文会保存大量实体和快照,内存压力很大。
for (Asset asset : assets) {
entityManager.persist(asset);
}优化:
for (int i = 0; i < assets.size(); i++) {
entityManager.persist(assets.get(i));
if (i % 500 == 0) {
entityManager.flush();
entityManager.clear();
}
}flush 把 SQL 发出,clear 清理持久化上下文,避免托管实体越积越多。
如果批量场景非常重,MyBatis、JDBC batch、数据库批量导入工具可能更合适。
商业场景:资产状态更新
不推荐:
public void updateAsset(Asset request) {
assetRepository.save(request);
}风险:请求对象字段不完整,可能覆盖数据库中已有字段。
推荐:
@Transactional
public void checkAsset(Long id) {
Asset asset = assetRepository.findById(id)
.orElseThrow(() -> new BizException("资产不存在"));
asset.check();
}实体方法:
public void check() {
if (!Objects.equals(this.status, WAIT_CHECK)) {
throw new BizException("状态不允许审核");
}
this.status = CHECKED;
this.checkedAt = LocalDateTime.now();
}这样做的好处:
- 先拿到托管实体。
- 状态规则在实体方法中收敛。
- 脏检查只更新真正变化。
- 不会被请求对象的 null 覆盖。
线上排查
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 自动发了很多 SQL | N+1 或懒加载 | 打开 SQL 日志,查调用栈 |
| 修改实体不落库 | 无事务或游离态 | 检查 @Transactional 和实体状态 |
| 接口返回时报错 | 懒加载异常 | Service 内转 DTO |
| save 后字段变 null | merge 覆盖 | 不直接 merge 请求对象 |
| 批量导入 OOM | 持久化上下文堆积 | 分批 flush/clear |
| 分页 count 慢 | Page 自动 count | 改 Slice 或优化 count |
代码 Demo:实体状态和脏检查
实体类:
@Entity
@Table(name = "article")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
public void changeTitle(String title) {
this.title = title;
}
}事务中修改实体:
@Transactional
public void renameArticle(Long id, String title) {
Article article = entityManager.find(Article.class, id);
article.changeTitle(title);
}article 被持久化上下文管理,事务提交时 Hibernate 会做脏检查,并生成对应的 update 语句。
为什么会自动更新
Hibernate 的核心原理是持久化上下文会保存托管实体的快照。事务提交时,Hibernate 对比当前对象和快照,如果发现字段变化,就生成对应 SQL。
flowchart TD
A["查询实体"] --> B["进入持久化上下文"]
B --> C["保存实体快照"]
C --> D["业务修改字段"]
D --> E["事务提交时脏检查"]
E --> F["生成 update SQL"]如果实体已经 detached,或者修改发生在事务外,就不会自动同步到数据库。这也是很多 Hibernate 问题的根源。
继续深入可以阅读 Hibernate/JPA 核心全过程原理,里面补充了 persist、merge、懒加载、N+1、事务边界、DTO 查询和线上排查流程。
面试标准回答
Hibernate 是 JPA 的常见实现,核心是 Session/持久化上下文、实体状态、快照、脏检查、flush 和懒加载。事务内查询或 persist 的实体会进入托管态,Hibernate 保存快照;flush 时比较当前实体和快照,发现变化就生成 SQL。flush 是同步 SQL,commit 是提交事务。懒加载通过代理实现,Session 关闭后访问代理会报 LazyInitializationException。列表循环访问懒加载关联会产生 N+1。生产中要返回 DTO,关注最终 SQL,批量操作要 flush/clear,复杂 SQL 和高性能批处理不一定适合 Hibernate。