JPA 实体状态与 detached(脱管)状态全解析
本文聚焦 JPA/Hibernate 中实体的生命周期状态,尤其是容易被误解的 detached(脱管/游离)状态,不依赖任何具体业务,所有示例均为通用代码。
一、为什么要有"实体状态"这个概念
JPA 的实体(Entity)不是普通的 Java 对象------它被 EntityManager(持久化上下文 / 会话) 管理。一个实体对象"当前处于什么状态",决定了你对它调用 save、修改字段、find 等操作会产生什么效果。
理解状态,是理解前面所有坑(save 不落库、自动更新、detached 异常、afterCommit 取不到)的基础。
二、四种核心状态
| 状态 | 中文 | 是否在持久化上下文 | 是否有 DB 行对应 | 典型来源 |
|---|---|---|---|---|
| Transient(瞬时) | 临时 | 否 | 否(无 ID) | new Entity() |
| Managed(受管) | 受管 | 是 | 是 | find / save 后(事务内) |
| Detached(脱管) | 游离 | 否 | 是(有 ID) | 事务关闭后、跨层传递的对象 |
| Removed(删除) | 已删 | 是 | 即将无 | remove() 后、提交前 |
注意:detached 不是"被删了",而是"它对应数据库里的一行,但当前会话已经不认识它了"。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、Transient(瞬时态)
java
User u = new User(); // 刚 new,没有 ID
u.setName("Tom");
// 此时 u 是 transient:EntityManager 完全不知道它的存在
特征:
- 对象存在于内存,但没有任何数据库行对应(ID 通常为 null)。
save(u)会把它变成 Managed 并最终INSERT。- 如果事务结束前没 save,对象随 GC 消失,数据库无痕迹。
四、Managed(受管态)
java
@Transactional
public void edit(Long id) {
User u = userRepository.findById(id).get(); // ← 返回的是 Managed
u.setName("Jerry"); // 不调 save,事务提交时自动 UPDATE
}
特征:
- 实体被当前
EntityManager持有(在持久化上下文里)。 - Hibernate 对该对象做脏检查(dirty checking) :事务提交时比对快照,有改动就自动
UPDATE。 save对 managed 实体是多余操作(但无害,走 merge 路径)。- 只有 managed 实体才享受"自动审计填充、自动脏检查、懒加载"等所有 JPA 便利。
五、Detached(脱管态)------ 重点
1. 定义
一个实体对象,曾经是 managed(或至少数据库里有一行对应它,有 ID),但当前 EntityManager 已经关掉 / 不包含它------这种状态叫 detached。
通俗理解:
"这个对象知道自己的 ID(知道自己是数据库哪一行),但它现在'脱单'了,没有会话管它。你对它的任何修改,JPA 都不会自动同步到数据库。"
2. 它是怎么产生的(5 种常见途径)
途径 A:事务方法返回后,调用方拿到对象
java
@Transactional
public User find(Long id) {
return userRepository.findById(id).get(); // 方法内是 managed
} // 方法结束 → 事务关闭 → 返回的对象变 detached
调用方拿到的是 detached:
java
User u = userService.find(1L);
u.setName("XX"); // 改了,但不会有任何 SQL,因为 u 已 detached
途径 B:把对象跨层传递(Controller / 异步线程 / afterCommit 回调)
java
User u = userRepository.findById(id).get(); // 事务内 managed
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// u 在这里是 detached(原事务已关)
u.getName(); // 读字段 OK(对象在内存)
// 但若想 repo.save(u) 写库 → 见下面的坑
}
});
途径 C:把 EntityManager 关掉 / clear
java
User u = repo.findById(1L).get(); // managed
entityManager.clear(); // 清空持久化上下文 → u 变 detached
u.setName("Y"); // 不会落库
途径 D:序列化/反序列化(网络传输、RPC、MQ 消息体)
java
// 实体经 JSON 序列化发给别的系统,再反序列化回来
User u = JSON.parse(json, User.class); // 反序列化出的对象一定是 detached(新对象,无会话)
这是最典型的 detached 来源:DTO/Entity 跨进程后必然 detached。
途径 E:从另一个 EntityManager 查出来,再拿到当前会话用
不同 EntityManager(不同事务/不同线程)之间,对象互不认识,跨过去就是 detached。
3. detached 实体的能力边界
| 操作 | detached 上是否生效 | 说明 |
|---|---|---|
读字段 u.getName() |
✅ 生效 | 对象在内存,随便读 |
改字段 u.setName() |
⚠️ 不生效 | 改了也不会自动同步 DB(无脏检查) |
repo.save(u) |
⚠️ 看情况 | 见下方"detached 的 save 陷阱" |
懒加载 u.getItems() |
❌ 抛异常 | Session 已关 → LazyInitializationException |
| 自动审计填充 | ❌ 不触发 | 见审计篇(afterCommit 取不到) |
六、detached 上调用 save 的陷阱
陷阱 1:detached entity passed to persist
java
User u = /* detached,有 ID=1 */;
userRepository.save(u);
sava 内部逻辑(简化):
- 若实体 无 ID →
persist(要求 transient) - 若实体 有 ID →
merge(把 detached 状态合并回会话)
但报错 detached entity passed to persist 通常发生在:
- ID 生成策略是
IDENTITY(自增),Hibernate 认为"有 ID 就应该是已存在的 managed",但对象却不在会话里 → 误走 persist 路径。 - 或关联对象 是 detached 且
cascade = PERSIST/ALL,级联把 detached 关联当 transient 处理。
陷阱 2:merge 后原对象还是 detached,新对象是 managed
java
User detached = /* 有 ID=1,改了 name */;
User merged = userRepository.save(detached);
// detached.name 的改动 → 在 merged 上生效并落库
// 但 detached 本身仍是 detached,它的改动不保证同步
关键点 :merge 返回的是新的 managed 实例 (或原 managed 的引用),传入的 detached 对象不会变成 managed。很多 bug 来自"以为 save 后原对象就同步了"。
正确写法:
java
User merged = userRepository.save(detached);
// 后续用 merged,不要用 detached
陷阱 3:detached 改了字段但忘了 merge → 丢失
java
User u = remoteCallGetUser(); // detached
u.setStatus("DONE");
// 没 save/merge → 数据库不变
七、如何"重新接管"detached 实体
方法 A:merge(重新关联)
java
@Transactional
public void fix(User detached) {
User managed = userRepository.save(detached); // merge 回会话
// 现在 managed 是 managed,提交时脏检查落库
}
注意:merge 会用 detached 的字段覆盖 DB 当前值,若期间别人改过,会覆盖(乐观锁可防)。
方法 B:find 后手动拷贝字段(更安全,避免误覆盖)
java
@Transactional
public void fix(Long id, User detached) {
User managed = userRepository.findById(id).get(); // 拿到 managed
managed.setName(detached.getName()); // 只拷需要的字段
managed.setStatus(detached.getStatus());
// 不调 save,脏检查自动 UPDATE
}
这是最推荐的方式:永远基于 find 得到的 managed 实体做修改,不把 detached 直接 save 回去。
方法 C:显式 lock(需乐观锁版本)
java
User managed = entityManager.merge(detached);
entityManager.lock(managed, LockModeType.OPTIMISTIC);
八、detached 与前面几篇坑的关系
| 前文主题 | 与 detached 的关联 |
|---|---|
| 审计取不到 | afterCommit 里传入的 DeliveryRecordMaster 是 detached,只读字段不写它;写通知日志用新实体,需手动填审计字段 |
| afterCommit 无事务 | afterCommit 回调里拿到的原事务对象全是 detached,且当前无会话,任何 save 需新事务 |
| save 不落库 | detached 上改字段不会自动同步(无脏检查),必须 merge/新事务 save |
| detached entity passed to persist | 把跨事务/detached 实体直接 save 进新事务时报错 |
九、DTO vs Entity 的边界建议
不要把 Entity 直接序列化跨层/跨进程传递------传出去再传回来必然是 detached,且暴露表结构。
java
// 推荐:用 DTO 承载数据
public class UserDto {
private Long id;
private String name;
// 只含需要的字段
}
// 接收 DTO → 在事务内 find → 拷贝 → 落库
这条规则能规避 90% 的 detached 相关事故。
十、状态转换速记图
new Entity()
│
▼
[Transient] ──── save/persist ────► [Managed] ◄─── find/query
│ │
│ │ 事务提交 / em.clear / 跨层
│ │
│ [Detached] ◄─── 返回给调用方 / 序列化
│ │
│ merge ──┘ (回到 Managed)
│
GC 消失 remove() ──► [Removed] ──► 提交后消失
十一、一句话总结
Detached(脱管)状态 = 实体知道自己的数据库 ID,但当前没有
EntityManager管它。 它存在于内存、能读字段、改字段不自动落库、懒加载会抛异常、save 可能报detached entity passed to persist。所有"事务方法返回后、跨层传递、afterCommit 回调、序列化反序列化"拿到的实体都是 detached。安全做法:在新事务里find出 managed 实体再改,或显式merge,永远别把 detached 实体当 managed 用。