如何解决 Hibernate 保存对象时的 TransientObjectException?
这个异常表示,Hibernate 在 flush 时发现某个实体引用了一个被判定为尚未保存的 Dashboard 对象。如何修复取决于这个对象的用途:新建对象需要先持久化;已有对象应从数据库取得受管引用;需要自动保存时,则应在正确的关联方向上配置级联。
仅凭给出的 Dashboard 类,还无法确定出错的属性。应先查清哪个实体引用了 Dashboard,以及何时给这个引用赋了值。只给 Dashboard 内部的关联添加 cascade = ALL,可能根本没有覆盖出错的路径。
从日志中能确定什么?
未保存对象的类型是 Dashboard
异常末尾的类型名:
com.quintiles.ra.model.Dashboard
表示 Hibernate 将关联目标判定为 transient,但没有指出哪个实体或属性持有这个引用。
例如,假设另一个实体有如下关联:
@ManyToOne
@JoinColumn(name = "DASHBOARD_ID")
public Dashboard getDashboard() {
return dashboard;
}
如果受管对象的 dashboard 属性指向尚未保存的 new Dashboard(),就可能在 flush 时触发异常。
排查时应搜索所有以 Dashboard 为目标的关联,包括其他实体、父类和 XML 映射中的配置。当前代码中的这两个方法:
public CategoryCollection getCategoryCollection()
public DashboardStyle getDashboardStyle()
关联的目标分别是 CategoryCollection 和 DashboardStyle。不能仅凭它们没有级联配置,就认定它们导致了这次指向 Dashboard 的异常。
查询触发了自动 flush
日志中有这样一段调用链:
DefaultAutoFlushEventListener.onAutoFlush
SessionImpl.autoFlushIfRequired
SessionImpl.list
CriteriaImpl.list
RaService.findProtocolQuarantine(RaService.java:2513)
这说明异常发生在查询前的自动 flush 阶段。Hibernate 此时会检查当前持久化上下文中待同步的修改,错误的关联可能早已在此前赋值。
findProtocolQuarantine() 第 2513 行是触发异常的位置。排查还需要向前追踪同一个 Session 或事务中创建、修改对象的代码,尤其要检查界面初始化时对关联属性的赋值。
ManyToOneType.isDirty() 提示,可以优先检查脏检查涉及的单值实体关联。它没有给出属性名,因此还不能直接锁定某个 getter。
为什么添加 cascade = ALL 后仍然报错?
级联有明确的方向
在 Dashboard.getTiles() 上配置级联,表示操作从 Dashboard 传播到 Tile:
Dashboard → Tile
这不会自动建立反向传播,也不会让其他引用 Dashboard 的对象自动保存它。
假设实际出错的路径是:
某个受管实体 → 尚未保存的 Dashboard
修改 Dashboard → DashboardStyle、Dashboard → QuickFilter 等关联,就无法解决这条引用路径上的问题。
级联类型需要与操作匹配
代码中导入的是 Hibernate 的枚举:
import org.hibernate.annotations.CascadeType;
JPA 关联注解的 cascade 参数使用的则是:
javax.persistence.CascadeType
两者不能混用。JPA 的写法例如:
@ManyToOne(cascade = javax.persistence.CascadeType.PERSIST)
Hibernate 原生写法则是:
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)
需要确认代码实际调用的是 persist()、merge()、save() 还是 saveOrUpdate(),并检查关联上使用了哪套级联规则。SAVE_UPDATE 与 JPA 的 PERSIST、MERGE 不能简单视为同一个操作。级联类型匹配后,还要确认实际操作能够沿关联路径到达这个对象。
如何定位具体实体和属性?
可以按以下顺序排查:
- 搜索所有引用
Dashboard的映射,以及new Dashboard()、setDashboard(...)等创建和赋值代码。 - 检查持有这些引用的对象是否由同一个事务管理,特别留意 DTO 转实体、复制对象和界面初始化逻辑。
- 检查出错对象的实际状态,不能只看它是否有 ID。
- 在可疑赋值后临时调用
session.flush(),逐步定位无效引用最早出现的位置。
可以先记录对象的 ID,以及它是否由当前 Session 管理:
LOGGER.debug(
"dashboardId={}, managed={}",
dashboard.getId(),
session.contains(dashboard)
);
这两个结果只能辅助判断:
- 当前
Dashboard使用生成主键,id == null是尚未持久化的重要线索。 session.contains(dashboard) == false也可能表示对象已经 detached,不能单独证明它是 transient。id != null不代表数据库中一定存在对应记录,也不代表对象属于当前Session。
如果仍无法定位,可以在 Hibernate 4.3.4 的以下方法处设置断点:
ForeignKeys.getEntityIdentifierIfNotUnsaved()
DefaultFlushEntityEventListener.dirtyCheck()
前者可以帮助确认哪个 Dashboard 实例被判定为未保存;后者可以查看正在接受脏检查的实体、属性名和属性值。必要时,同时比较当前值与加载时的快照值,找出具体的关联属性。
临时调用 flush() 会执行数据库同步,应在可控的调试事务中使用。它只能让问题提前暴露,不能替代正确的持久化处理。
根据 Dashboard 的用途选择修复方式
以下示例都假设事务已经开启,holder 是实际持有 Dashboard 引用的实体,且已由当前 Session 管理。
Dashboard 是本次新建的对象
先显式持久化,再建立关联:
Dashboard dashboard = new Dashboard();
// 按实际映射补齐必填属性和必需关联。
session.persist(dashboard);
holder.setDashboard(dashboard);
session.flush();
persist() 会使对象进入持久化上下文,实际 SQL 的执行时机由 Hibernate 管理,无须在持久化和建立关联之间额外提交事务。
如果新建的 Dashboard 还引用了其他新实体,也需要为这些关联选择显式保存或合适的级联。
Dashboard 已经存在于数据库
从当前 Session 加载对象:
Dashboard dashboard =
(Dashboard) session.get(Dashboard.class, dashboardId);
if (dashboard == null) {
throw new IllegalArgumentException(
"Dashboard 不存在,id=" + dashboardId
);
}
holder.setDashboard(dashboard);
不要用下面的方式代替加载:
Dashboard dashboard = new Dashboard();
dashboard.setId(dashboardId);
手工填写 ID 不会使对象自动成为受管对象,还可能干扰 Hibernate 对新旧状态的判断。
如果明确知道记录存在,也可以使用 Hibernate 4.3 的 session.load()。它可能返回代理,记录不存在的错误可能要到访问时才出现。
Dashboard 是需要合并修改的 detached 对象
如果业务需要保存这个对象的修改,可以调用 merge(),并使用它的返回值:
Dashboard managedDashboard =
(Dashboard) session.merge(detachedDashboard);
holder.setDashboard(managedDashboard);
merge() 返回受管实例,传入的原对象不会因此自动成为受管对象。如果只是建立关联,无须合并修改,通常直接加载已有记录更清楚。
业务确实要求自动保存 Dashboard
假设某个实体负责创建新的 Dashboard,可以在该实体指向 Dashboard 的关联上配置:
@ManyToOne(cascade = javax.persistence.CascadeType.PERSIST)
@JoinColumn(name = "DASHBOARD_ID")
public Dashboard getDashboard() {
return dashboard;
}
这里的属性仅用于说明级联的配置位置和类型,当前项目中未必存在这个属性。如果还需要传播 merge(),再按业务需要添加 MERGE。
不要为了消除异常统一使用 ALL。它包含 REMOVE,如果 Dashboard 被多个对象共享,删除级联可能造成误删或外键约束异常。
给出的 Dashboard 映射还有什么需要检查?
getImage() 直接创建实体的行为也需要单独处理:
public DashboardImage getImage() {
if (image == null) {
image = new DashboardImage();
}
return image;
}
从当前注解布局看,实体使用属性访问。Hibernate 读取映射属性时可能调用这个 getter,使一次普通读取创建出新的关联实体。
建议让 getter 只返回当前状态:
public DashboardImage getImage() {
return image;
}
@Transient
public byte[] getImageData() {
DashboardImage currentImage = getImage();
return currentImage == null ? null : currentImage.getData();
}
图片对象应在明确的业务操作中创建并建立关联,同时需要调整依赖自动创建行为的 setImageData()。
@PrimaryKeyJoinColumn 涉及主键关联,还需要结合 DashboardImage 的主键生成方式、关联映射和数据库约束一起检查。optional = false 不会自动创建或保存图片对象。
这次异常点名的是 Dashboard,目前还不能确定图片映射就是根因。应先找到指向未保存 Dashboard 的具体引用,再检查图片关联是否参与了这条对象路径。
修复后的验证方式
在同一事务中完成对象创建和关联赋值后,显式调用一次 session.flush(),确认问题已在查询前解决,再恢复正常业务流程。
关闭自动 flush 或调整 LAZY、EAGER 都不能把 transient 对象变成持久化对象,因此不应靠这些设置掩盖异常。flush 失败后,应回滚当前事务,并按应用的会话管理方式结束该持久化上下文。
要确定最终应修改哪一处映射,还需要查看引用 Dashboard 的实体、实际保存代码,以及 findProtocolQuarantine() 执行前对这些对象所做的修改。
备注:内容仅供参考。
