JPA和Hibernate抛出的PersistentObjectException: detached entity passed to persist异常

339

我有一个使用JPA持久化的对象模型,其中包含一个多对一的关系:一个 账户(Account) 拥有许多 交易(Transactions)。一个 交易(Transaction) 只属于一个 账户(Account)

以下是代码片段:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

我可以创建一个Account对象,向其添加交易并正确持久化Account对象。但是,当我创建一笔交易(使用一个已经持久化的现有Account)并持久化该交易时,会出现异常:

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141)

所以,我能够持久化包含交易的Account,但不能持久化具有Account交易。我原以为这是因为Account可能未被连接,但即使使用这个代码,仍然会出现相同的异常:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

如何正确保存已持久化的Account对象相关的Transaction


27
在我的情况下,我正在尝试使用实体管理器持久化一个实体的 ID。当我移除了 ID 的 setter 后,它开始正常工作。 - Rushi Shah
3
在我的情况下,我没有设置id,但有两个用户使用同一个账户,其中一个正确地持久化了实体,当第二个用户尝试持久化已经持久化的相同实体时,就会出现错误。 - sergioFC
24个回答

397

解决方案很简单,只需要使用CascadeType.MERGE而不是CascadeType.PERSISTCascadeType.ALL

我曾经遇到过相同的问题,CascadeType.MERGE对我有效。

希望这能解决你的问题。


10
令人惊讶的是,那个对我也起作用了。这毫无意义,因为CascadeType.ALL包括所有其他级联类型... 什么鬼?我使用的是spring 4.0.4、spring data jpa 1.8.0和hibernate 4.X.. 有人有想法为什么ALL不行,但MERGE可以吗? - Vadim Kirilchuk
31
这对我也起作用了,而且这完全合理。由于Transaction是PERSISTED,它也试图持久化Account,但这不起作用,因为Account已经在数据库中了。但是使用CascadeType.MERGE,Account会自动合并。 - Gunslinger
6
如果您不使用事务,就有可能发生这种情况。 - lanoxx
1
另一个解决方案:尝试不要插入已经持久化的对象 :) - Andrii Plotnikov
4
谢谢,如果您对参考键的限制为 NOT NULL,则无法避免插入持久化对象,这是唯一的解决方案。再次感谢。 - makkasi
显示剩余7条评论

154

这是一个典型的双向一致性问题。该问题在此链接此链接中有详细讨论。

根据前两个链接中的文章,您需要在双向关系的两端修复setter方法。一个One端的示例setter方法在此链接中。

Many端的一个示例setter方法在此链接中。

在更正了setter方法之后,您希望声明实体访问类型为“Property”。声明“Property”访问类型的最佳实践是将所有注释从成员属性移动到相应的getter方法中。 强烈警告:不要在实体类中混合使用“Field”和“Property”访问类型,否则行为将未定义JSR-317规范。


2
ps:@Id注释是Hibernate用于标识访问类型的注释。 - Diego Plentz
2
异常是:传递给persist的分离实体 为什么提高一致性会使它工作?好的,一致性已经修复,但对象仍然是分离的。 - Gilgamesz
157
请勿发布仅包含链接的答案。 - El Mac
12
不明白这个回答与问题有何关联? - Eugen Labun
1
低质量的回答。仅提供链接到大型网页,没有具体说明在哪里工作是相当无用的。 - evilfred
显示剩余12条评论

63

移除子实体Transaction的级联操作,应该只有:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}
(FetchType.EAGER 可以被移除,因为它是 @ManyToOne 的默认值。)
那就是全部内容了!
为什么?在子实体 Transaction 上说“级联所有(cascade ALL)”会要求每个数据库操作都被传播到父实体 Account。如果你接下来执行 persist(transaction)persist(account) 也会被调用。
但只有瞬态(新的)实体可以被传递给 persist(在这种情况下是 Transaction)。已分离(或其他非瞬态状态)的实体则不可以(在这种情况下是 Account,因为它已经在数据库中)。
因此你会收到异常信息 "detached entity passed to persist"。这里指的是 Account 实体!而不是你在其上调用 persistTransaction
通常你不想从子实体传播到父实体。不幸的是在书籍(甚至是好的书籍)和网络中有许多代码示例都会这样做。我不知道原因是什么……可能有时候只是被简单地一遍又一遍地复制……
猜猜如果你仍然保留那个 @ManyToOne 中的 “cascade ALL”,然后调用 remove(transaction) 会发生什么?account(还有所有其他交易!)也将从数据库中删除。但这不是你的意图,是吧?

只是想补充一下,如果您的意图确实是保存孩子和父母,并删除父母和孩子,例如由数据库自动生成的人(父母)和地址(孩子)以及addressId,则在调用Person的save之前,在您的事务方法中先调用address的save。这样它将与由DB生成的id一起保存。对性能没有影响,因为Hibenate仍然会进行2个查询,我们只是改变了查询的顺序。 - Vikky
如果我们无法传递任何内容,那么它将采用哪个默认值来处理所有情况? - Dhwanil Patel
@Eugen Labun 的解释非常棒。 - Maurice

36

移除子关联级联

因此,您需要从@ManyToOne关联中移除@CascadeType.ALL。子实体不应级联到父关联。只有父实体应级联到子实体。

@ManyToOne(fetch= FetchType.LAZY)

注意我将fetch属性设置为FetchType.LAZY,因为急切获取会导致性能非常差。

设置关联的两个方向

当你有一个双向关联时,你需要在父实体中使用addChildremoveChild方法同步双方:

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

如果我们添加带有@Prepersist注释的方法,在该方法中仅将“this”引用设置在所有子实体中,而不是进行管理,这样做如何?void prePersist(){ transactions.foreach( t -> t.setAccount(this)) - Faizan
1
@FaizanAhmad 这并不能处理当你添加子元素时未将其添加到父元素的情况。 - Vlad Mihalcea

35

不要将id(pk)传递给persist方法,而应该尝试使用save()方法代替persist()。


2
好建议!但仅当id被生成时。如果它是被分配的,那么设置id是很正常的。 - Aldian
这对我有用。此外,您可以使用TestEntityManager.persistAndFlush()来使实例受管理并持久化,然后将持久化上下文与底层数据库同步。返回原始源实体。 - Anyul Rivas
1
这个像魔法一样奏效了。 - anshulkatta

17
使用merge是有风险且棘手的,所以在你的情况下,它是一种不正规的解决方法。你至少需要记住,当你将一个实体对象传递给merge时,它会停止与事务的关联,并返回一个新的已关联实体对象。这意味着,如果其他人仍然持有旧实体对象,对它所做的更改将在提交时被悄无声息地忽略和丢弃。
由于您没有显示完整的代码,因此我无法检查您的事务模式。出现这种情况的一种方式是,当执行merge和persist时没有活动事务。在这种情况下,持久性提供程序会为您执行的每个JPA操作打开一个新的事务,并在调用返回之前立即提交和关闭它。如果是这种情况,merge将在第一个事务中运行,然后在merge方法返回后,事务完成并关闭,返回的实体现在是分离的。接下来的persist将打开第二个事务,并尝试引用一个已分离的实体,从而导致异常。除非您非常清楚自己在做什么,否则始终在事务内部包装您的代码。
使用容器管理事务看起来像这样。请注意:这假设该方法位于会话bean中,并通过本地或远程接口调用。
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}

我遇到了同样的问题。我在服务层使用了@Transaction(readonly=false),但仍然遇到了相同的问题。 - Senthil Arumugam SP
1
我不能说我完全理解为什么事情会这样运作,但是将persist方法和view到实体映射的操作放在一个Transactional注释中解决了我的问题,所以谢谢。 - Deadron
1
这实际上是比得到最多赞的解决方案更好的一个。 - Aleksei Maide
1
我们应该管理持久化上下文,以保持实体处于受控状态,而不是绕过去更改实体本身。 - Yu Tian Toby

14

很可能在这种情况下,您使用了合并逻辑获取了account对象,并且persist用于持久化新对象,如果层次结构中已经有一个持久化的对象,它将抱怨。在这种情况下,应该使用saveOrUpdate而不是persist


3
是JPA,所以我认为类似的方法是.merge(),但它仍然引发相同的异常。需要明确的是,Transaction是一个新对象,Account不是。 - Paul Sanwald
实际上,不,我说错了。如果我 .merge(transaction),那么 transaction 根本没有被持久化。 - Paul Sanwald
@PaulSanwald 嗯,你确定 transaction 没有被持久化吗?你是怎么检查的?请注意,merge 返回的是对已持久化对象的引用。 - dan
由于 .merge() 返回的对象具有空 id,因此我随后进行了 .findAll() 操作,但我的对象并不存在。 - Paul Sanwald
@PaulSanwald 你可以在 merge 后尝试使用 flush() 吗? - dan
显示剩余6条评论

9

虽然这是一个老问题,但我最近遇到了相同的问题。在这里分享我的经验。

实体

@Data
@Entity
@Table(name = "COURSE")
public class Course  {

    @Id
    @GeneratedValue
    private Long id;
}

保存实体(JUnit)
Course course = new Course(10L, "testcourse", "DummyCourse");
testEntityManager.persist(course);

修复

Course course = new Course(null, "testcourse", "DummyCourse");
testEntityManager.persist(course);

结论:如果实体类的主键(id)上有 @GeneratedValue 注解,则确保不为主键(id)传递值。


1
这是帮助了我的答案,非常感谢!! - Peter Petrekanics

9

谢谢!在我的情况下,我不得不将我的@Transactional方法拆分为两个单独的@Transactional方法才能使其工作。 - ntg
谢谢,上述数据是准确的。我们需要绑定保存方法。 - Pratik Gaurav

6
如果没有任何帮助,而且您仍然遇到此异常,请检查您的equals()方法 - 不要在其中包含子集合。特别是如果您有嵌套集合的深层结构(例如A包含B,B包含C等)。
Account -> Transactions为例:
  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

在上面的例子中,从equals()检查中删除事务。这是因为Hibernate会认为您不是尝试更新旧对象,而是将新对象传递给持久化,每当更改子集合中的元素时。
当然,这种解决方案并不适用于所有应用程序,您应该仔细设计要包含在equalshashCode方法中的内容。

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