JPA / Hibernate:传递给persist的分离实体

我有一个包含多对一关系的JPA持久化对象模型:一个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; 

我能够创build一个帐户对象,添加交易,并坚持正确的帐户对象。 但是,当我创build一个事务, 使用一个已经存在的已经存在的Account ,并且保存 了Transaction时 ,我得到一个exception:

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

所以,我可以坚持一个包含交易的账户,但不能持有一个账户的交易。 我以为这是因为该帐户可能不附加,但是这个代码仍然给我同样的例外:

 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); 

我如何正确地保存一个交易,与一个已经持续的账户对象相关联?

这是一个典型的双向一致性问题。 这个链接以及这个链接都有很好的讨论。

根据前两个链接中的文章,您需要在双向关系的两侧修复您的setter。 在这个链接中,一方的示例设置者。

许多方面的示例二传手在这个链接。

编辑

我希望这个编辑对任何人来说都不算太晚。 在更正你的setter之后,你要声明Entity访问types为“Property”。 声明“属性”访问types的最佳做法是将所有注释从成员属性移动到相应的获取器。 值得警惕的是,不要在实体类中混合使用“字段”和“属性”types的访问types,否则JSR-317规范不会定义行为。

解决scheme很简单,只需使用CascadeType.MERGE而不是CascadeType.PERSISTCascaCadeType.ALL

我有同样的问题, CascadeType.MERGE已经为我工作。 我希望你sorting。

可能在这种情况下,您使用合并逻辑获得了您的account对象, persist用于保留新对象,如果层次结构具有已保存的对象,则会抱怨。 在这种情况下,您应该使用saveOrUpdate ,而不是persist

使用合并是有风险和棘手的,所以在你的情况下这是一个肮脏的解决方法。 您至less应该记住,当您传递一个实体对象进行合并时,它将不再被附加到事务中,而是返回一个新的,即时连接的实体。 这意味着,如果任何人有旧的实体对象仍然拥有,对它的改变被默默地忽略,并在提交时被抛弃。

你没有在这里显示完整的代码,所以我不能仔细检查你的交易模式。 解决这种情况的一种方法是,如果在执行合并和保持时没有事务处于活动状态。 在这种情况下,持久性提供程序需要为您执行的每个JPA操作打开一个新的事务,并在调用返回之前立即提交并closures它。 如果是这种情况,合并将在第一个事务中运行,然后在合并方法返回后,事务完成并closures,返回的实体现在分离。 坚持下面它会打开第二个事务,并试图引用一个被分离的实体,给出一个例外。 除非你非常清楚你在做什么,否则总是把你的代码包装在一个事务中。

在您的实体定义中,您没有为join到TransactionAccount指定@JoinColumn 。 你会想要这样的东西:

 @Entity public class Transaction { @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER) @JoinColumn(name = "accountId", referencedColumnName = "id") private Account fromAccount; } 

编辑:好吧,我想这将是有用的,如果你在你的课堂上使用@Table注解。 嘿。 🙂

如果没有任何帮助,你仍然得到这个exception,审查你的equals()方法 – 不要包括子集合。 特别是如果你有embedded式集合的深层结构(例如A包含Bs,B包含Cs等)。

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意味着你不是试图更新旧对象,而是当你更改子集合上的元素时,你传递一个新对象来坚持。
当然这个解决scheme不适合所有的应用程序,你应该仔细devise你想要包含在equalshashCode方法中。

也许这是OpenJPA的错误,当回滚它重置@Version字段,但pcVersionInit保持真实。 我有一个AbstraceEntity声明了@Version字段。 我可以通过重置pcVersionInit字段来解决这个问题。 但这不是一个好主意。 我认为它没有工作时,级联坚持实体。

  private static Field PC_VERSION_INIT = null; static { try { PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit"); PC_VERSION_INIT.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { } } public T call(final EntityManager em) { if (PC_VERSION_INIT != null && isDetached(entity)) { try { PC_VERSION_INIT.set(entity, false); } catch (IllegalArgumentException | IllegalAccessException e) { } } em.persist(entity); return entity; } /** * @param entity * @param detached * @return */ private boolean isDetached(final Object entity) { if (entity instanceof PersistenceCapable) { PersistenceCapable pc = (PersistenceCapable) entity; if (pc.pcIsDetached() == Boolean.TRUE) { return true; } } return false; } 

即使您的注释被正确地声明以正确pipe理一对多关系,您仍然可能会遇到这种精确的exception。 当添加一个新的子对象, Transaction ,到一个附加的数据模型,你需要pipe理主键值 – 除非你不应该 。 如果您在调用persist(T)之前为以下声明的子实体提供主键值,则会遇到此exception。

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

在这种情况下,注释声明数据库将在插入时pipe理实体主键值的生成。 自己提供一个(例如通过Id的setter)导致这个exception。

另外,但实际上相同,这个注释声明导致相同的exception:

 @Entity public class Transaction { @Id @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid") @GeneratedValue(generator="system-uuid") private Long id; .... 

所以,当它已经被pipe理的时候,不要在你的应用程序代码中设置id值。

您需要为每个帐户设置交易。

 foreach(Account account : accounts){ account.setTransaction(transactionObj); } 

或者它足够(如果适当的话)将id设置为null。

 // list of existing accounts List<Account> accounts = new ArrayList<>(transactionObj.getAccounts()); foreach(Account account : accounts){ account.setId(null); } transactionObj.setAccounts(accounts); // just persist transactionObj using EntityManager merge() method.