如何修复Hibernate中的“对象引用了未保存的瞬态实例 - 在刷新之前保存瞬态实例”错误。

884

我使用Hibernate保存对象时遇到以下错误:

object references an unsaved transient instance - save the transient instance before flushing

2
这个错误出现在什么上下文中?是与瞬态变量有关还是没有? - vijay
1
如果您的对象之间存在关联关系,确保它们按正确顺序保存。例如:作者在书之前保存,因为作者是书的必要条件。同样地。 - Gayal Rupasinghe
33个回答

1099
你应该在集合映射上包含cascade="all" (如果使用xml)或cascade=CascadeType.ALL(如果使用注释)。
这是因为你的实体中有一个集合,而该集合具有一个或多个数据库中不存在的项目。通过指定上述选项,您告诉Hibernate在保存它们的父项时将它们保存到数据库中。

11
这不是默认的吗?你总是希望Hibernate保存它们,对吧? - Marcus Leon
46
@Marcus - 不是的,你可能需要手动处理它们。 - Bozho
5
博兹霍是对的。我遇到过这样的情况,因为集合很大,或者由于业务规则不允许在同一时间保存集合中的所有对象,所以我想手动管理它们。 - Alex Marshall
46
不仅在集合中发生这种情况,也发生在简单的一对一映射中。 - Sebastien Lorber
18
从CascadeType.PERSIST开始并使用persist进行保存,这样不是更好吗? - Sergii Shevchyk
显示剩余10条评论

361

我认为这可能只是重复的答案,但是为了澄清,无论是在@OneToOne映射还是在@OneToMany映射中,问题都在于我要添加到ParentChild对象还没有保存在数据库中。因此,当我将Child添加到Parent中,然后保存Parent时,Hibernate会抛出"object references an unsaved transient instance - save the transient instance before flushing"消息。

ParentChild的引用上添加cascade = {CascadeType.ALL}可以解决这两种情况下的问题。这将保存ChildParent

很抱歉如果有重复的答案,只是想进一步为大家澄清。

@OneToOne(cascade = {CascadeType.ALL})
@JoinColumn(name = "performancelog_id")
public PerformanceLog getPerformanceLog() {
    return performanceLog;
}

14
如果我不想在@OneToOne关系中级联保存,那该怎么办?在第一次创建两个对象时,如何将任一对象保存到数据库而不触发异常? - xtian
6
如果我只想在某些情况下拯救孩子,而在其他情况下不想拯救,该怎么办? - Omaruchan
1
@xtian:那么你就必须通过使用EntityManager持久化对象来确保正确的保存顺序。基本上,你只需要这样写 em.persist(object1);em.persist(object2);等等。 - kaba713
我在使用@Inheritance时遇到了这个问题,具体来说是TABLE_PER_CLASS情况下,我正在引用一个子类。CascadeType.ALL修复了它。 - Jim ReesPotter
或者你使用 new MyEntity 创建了实体对象(没有将其与数据库同步 - 刷新),而不是从数据库中获取它的同步实例。使用该实例进行 Hibernate 查询会告诉你,你期望在数据库中的内容与你在应用程序内存中拥有的内容不同。在这种情况下 - 只需从数据库同步/获取你的实体实例并使用它即可。然后就不需要使用 CascadeType.ALL。 - Zon
1
我在一台机器上遇到了这个问题,但在其他机器上运行正常。一旦我使用childRepo.save(child)保存子对象,然后再保存parentRepo.save(parent),它就在所有机器上都能正常工作。不知道只保存父对象时出了什么问题。有什么建议吗? - pjsagar

193

介绍

使用JPA和Hibernate时,实体可以处于以下4种状态之一:

  • 新建 - 从未与Hibernate Session(即持久性上下文)关联且未映射到任何数据库表行的新创建对象被认为处于新建或临时状态。

要进行持久化,我们需要显式调用persist方法或利用传递性持久性机制。

  • 持久化 - 持久实体已经与数据库表行关联,并由当前运行的Persistence Context进行管理。

对此类实体所做的任何更改都将被检测并传播到数据库(在Session刷新时间内)。

  • 分离 - 当前运行的Persistence Context关闭后,所有先前管理的实体都变为分离状态。后续更改将不再被跟踪,也不会发生自动数据库同步。

  • 删除 - 虽然JPA要求只允许删除受管理的实体,但Hibernate也可以删除分离的实体(但仅通过remove方法调用)。

实体状态转换

要将实体从一种状态转移到另一种状态,可以使用persistremovemerge方法。

JPA entity states

解决问题

您在问题中描述的问题:

object references an unsaved transient instance - save the transient instance before flushing

由于将一个处于新建状态的实体与一个处于已管理状态的实体关联而引起的问题。

当您将子实体与父实体中的一对多集合相关联时,且该集合未级联实体状态转换时,就可能会发生此情况。

因此,您可以通过在触发此故障的实体关联上添加级联来解决此问题,具体如下:

@OneToOne 关联

@OneToOne(
    mappedBy = "post",
    orphanRemoval = true,
    cascade = CascadeType.ALL)
private PostDetails details;

请注意我们为cascade属性添加的CascadeType.ALL值。

@OneToMany关联

@OneToMany(
    mappedBy = "post", 
    orphanRemoval = true,
    cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();

再次强调,CascadeType.ALL适用于双向@OneToMany关联。

现在,在双向关联中使级联功能正常工作,您还需要确保父子关联保持同步。

@ManyToMany关联

@ManyToMany(
    mappedBy = "authors",
    cascade = {
        CascadeType.PERSIST, 
        CascadeType.MERGE
    }
)
private List<Book> books = new ArrayList<>();

@ManyToMany关联中,不能使用CascadeType.ALLorphanRemoval,因为这将从一个父实体传播删除实体状态转换到另一个父实体。

因此,在@ManyToMany关联中,通常级联CascadeType.PERSISTCascadeType.MERGE操作。或者,您可以扩展到DETACHREFRESH


Hibernate 如何确定实例已经分离?它只是查看 ID,如果为 null,则认为该实例已分离,还是还有其他因素涉及? - ACV
2
对于合并操作,它会运行一个SELECT查询语句。否则,它会检查id和version。 - Vlad Mihalcea
亲爱的 Vlad,我读了下面大部分的评论,对我来说就像下面答案中写的一样:https://dev59.com/NXE95IYBdhLWcg3wdtqj#40769546。你认为这是一个有效的解决方案吗?(对我来说,它似乎起作用了)。谢谢! - Alex Mi
1
调用 mergepersist 一个对象是一个不好的想法。仅仅因为它偶然能够工作,并不意味着这是正确的方式。 - Vlad Mihalcea

86

当Hibernate认为需要保存与正在保存的对象相关联的对象时,会出现此问题。

我遇到了这个问题,不想保存对相关对象所做的更改,因此希望级联类型为NONE。

诀窍是确保设置了引用对象中的ID和版本,以便Hibernate不认为引用对象是需要保存的新对象。这对我很有用。

查看您要保存的类中的所有关系,以确定相关对象(以及相关对象的相关对象),并确保在对象树中的所有对象中设置了ID和VERSION。


5
这条评论让我找到了正确的方向。我曾经将父对象的一个新实例赋值给其子对象的属性,因此NH认为它们是不同的实例。 - elvin
3
是的,如果关联对象的ID未包含(例如被@JsonIgnore忽略),就会发生这种情况。Hibernate无法识别关联实体,因此会尝试保存它。 - Rori Stumpf
谢谢!我有一个外键引用的不是被引用表上的ID字段,而是一个带有唯一约束(CODE)的字段,当创建Hibernate时无法从给定的CODE找到现有行并尝试在被引用的表上创建它。我通过从其CODE预取整个实体(包括ID),然后将其放入Java实体中保存来解决这个问题。这次Hib没有尝试创建该行,错误消失了。 - PJ_Finnegan

34
在我的情况下,这是由于在双向关系的@ManyToOne一侧没有设置CascadeType引起的。更准确地说,我在@OneToMany侧设置了CascadeType.ALL而在@ManyToOne上没有设置。将CascadeType.ALL添加到@ManyToOne中解决了这个问题。 一对多的一侧:
@OneToMany(cascade = CascadeType.ALL, mappedBy="globalConfig", orphanRemoval = true)
private Set<GlobalConfigScope>gcScopeSet;

多对一方(引起问题)

@ManyToOne
@JoinColumn(name="global_config_id")
private GlobalConfig globalConfig;

多对一(通过添加CascadeType.PERSIST进行修复)

@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name="global_config_id")
private GlobalConfig globalConfig;

3
如果我这样做,并保存父实体,那么会在我的父表中进行两次插入,即两行数据。我认为这是因为我们在父实体和子实体上都使用了级联操作? - theprogrammer

34

或者,如果你想使用最少的“权限”(例如,如果你不想进行级联删除)来实现你想要的结果,请使用

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;

...

@Cascade({CascadeType.SAVE_UPDATE})
private Set<Child> children;

15
这应该是被接受的答案。CascadeType.ALL 太宽泛了。 - lilalinux
8
截至Hibernate 5.2.8版本,似乎没有办法通过JPA注释实现相同的效果。例如,@ManyToMany(cascade={PERSIST, MERGE, REFRESH, DETACH})(除了REMOVE之外的所有操作)无法像Hibernate的CascadeType.SAVE_UPDATE那样级联更新。 - jcsahnwaldt Reinstate Monica

22

在我持久化一个实体时,发生了这种情况,数据库中现有记录对于使用@Version(用于乐观锁定)注释的字段具有NULL值。在数据库中将NULL值更新为0后,问题得到了纠正。


这应该是一个新问题,并且应该被添加为一个错误,至少是误导性的异常。这最终成为了我的问题的原因。 - BML
这解决了我的问题。 - Jad Chahine

13

这不是错误的唯一原因。我刚刚在我的代码中发现了一个打字错误,我相信这个错误设置了一个已经保存的实体的值。

X x2 = new X();
x.setXid(memberid); // Error happened here - x was a previous global entity I created earlier
Y.setX(x2);

我通过找到导致错误的变量(在这种情况下为String xid)来发现了错误。我使用了一个catch包围代码块保存实体并打印跟踪信息。

{
   code block that performed the operation
} catch (Exception e) {
   e.printStackTrace(); // put a break-point here and inspect the 'e'
   return ERROR;
}

1
类似于我的问题。毕竟,当我在本地重新加载实体,设置属性,然后保存时,它就可以正常工作了。 - CsBalazsHungary

13

除非确实需要,否则不要使用 Cascade.AllRolePermission 之间存在双向的manyToMany关系。那么以下代码将会正常工作。

    Permission p = new Permission();
    p.setName("help");
    Permission p2 = new Permission();
    p2.setName("self_info");
    p = (Permission)crudRepository.save(p); // returned p has id filled in.
    p2 = (Permission)crudRepository.save(p2); // so does p2.
    Role role = new Role();
    role.setAvailable(true);
    role.setDescription("a test role");
    role.setRole("admin");
    List<Permission> pList = new ArrayList<Permission>();
    pList.add(p);
    pList.add(p2);
    role.setPermissions(pList);
    crudRepository.save(role);

如果对象只是一个“新”对象,那么它会抛出相同的错误。


2
100%正确,Cascade.All是懒惰的解决方案,只有在必要时才应用。首先,如果实体已经存在,请检查它是否在当前实体管理器中加载,如果没有,请加载它。 - Renato Mendes

9

除了其他好的答案,如果您使用merge来持久化一个对象并意外忘记在父类中使用合并引用,则可能会发生这种情况。请考虑以下示例:

merge(A);
B.setA(A);
persist(B);

在这种情况下,您合并了A,但忘记使用合并后的A对象。为了解决问题,您必须像这样重写代码。
A=merge(A);//difference is here
B.setA(A);
persist(B);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接