我有一个包含多对一关系的jpa持久化对象模型:一个Account有多个transaction。一个事务有一个帐户。

下面是一段代码:

@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对象。但是,当我创建一个事务,使用现有的已经持久化的帐户,并持久化的事务,我得到一个异常:

导致:org.hibernate.PersistentObjectException:传递给persist: com.paulsanwald.Account的分离实体 org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java: 141)

因此,我能够持久化一个包含事务的Account,但不能持久化一个具有Account的Transaction。我认为这是因为帐户可能没有附加,但这段代码仍然给了我相同的异常:

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

如何正确地保存与已经持久化的帐户对象相关联的事务?


当前回答

您需要为每个帐户设置事务。

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.

其他回答

如果没有任何帮助,并且您仍然得到这个异常,请检查equals()方法—并且不要在其中包含子集合。特别是当你有嵌入式集合的深层结构时(例如A包含B, B包含c,等等)。

以Account ->为例:

  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将暗示您不尝试更新旧对象,而是在更改子集合上的元素时传递一个新对象来持久化。 当然,这种解决方案并不适用于所有应用程序,您应该仔细设计想要包含在equals和hashCode方法中的内容。

使用合并是有风险和棘手的,所以在您的情况下,这是一种肮脏的变通方法。您至少需要记住,当您将一个实体对象传递给merge时,它将停止附加到事务,而是返回一个新的、现在已附加的实体。这意味着如果任何人仍然拥有旧的实体对象,那么对它的更改将被无声地忽略并在提交时丢弃。

You are not showing the complete code here, so I cannot double-check your transaction pattern. One way to get to a situation like this is if you don't have a transaction active when executing the merge and persist. In that case persistence provider is expected to open a new transaction for every JPA operation you perform and immediately commit and close it before the call returns. If this is the case, the merge would be run in a first transaction and then after the merge method returns, the transaction is completed and closed and the returned entity is now detached. The persist below it would then open a second transaction, and trying to refer to an entity that is detached, giving an exception. Always wrap your code inside a transaction unless you know very well what you are doing.

使用容器管理的事务,它看起来像这样。注意:这假设方法在会话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时,您需要管理主键值—除非您不需要这样做。如果在调用persist(T)之前为如下声明的子实体提供主键值,则会遇到此异常。

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

在本例中,注释声明数据库将在插入时管理实体主键值的生成。自己提供一个(比如通过Id的setter)会导致此异常。

或者,但实际上是一样的,这个注释声明会导致相同的异常:

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

因此,当应用程序代码已经被管理时,不要在应用程序代码中设置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.

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

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

在此链接中有一个用于多方的示例setter。

在你纠正你的setter之后,你想要声明实体访问类型为“属性”。声明“Property”访问类型的最佳实践是将所有注释从成员属性移动到相应的getter。一个重要的警告是不要在实体类中混合使用“Field”和“Property”访问类型,否则JSR-317规范没有定义行为。