首页 文章

JPA / Hibernate:传递给持久化的分离实体

提问于
浏览
155

我有一个JPA持久化对象模型,它包含多对一关系:一个帐户有很多交易 . 交易有一个帐户 .

这是代码的片段:

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

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

如何正确保存与已持久的Account对象关联的事务?

11 回答

  • 3
    cascadeType.MERGE,fetch= FetchType.LAZY
    
  • 4

    如果没有任何帮助,并且您仍然遇到此异常,请查看您的 equals() 方法 - 并且不要在其中包含子集合 . 特别是如果你有深入的嵌入式集合结构(例如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将暗示您不是在尝试更新旧对象,而是在更改子集合上的元素时传递新对象以保持持久性 .
    当然,此解决方案不适合所有应用程序,您应该仔细设计要包含在 equalshashCode 方法中的内容 .

  • 9

    不要将id(pk)传递给persist方法或尝试使用save()方法而不是persist() .

  • 96

    这是典型的双向一致性问题 . 它在this link以及this link.中得到了很好的讨论 .

    根据前两个链接中的文章,您需要在双向关系的两侧修复您的setter . 一方的示例设置器位于this link.

    多边的示例设置器位于this link.

    在更正setter之后,您要将Entity访问类型声明为“Property” . 声明“属性”访问类型的最佳做法是将所有注释从成员属性移动到相应的getter . 值得注意的是,不要在实体类中混合使用“Field”和“Property”访问类型,否则JSR-317规范不会定义行为 .

  • 1

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

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

    或者它的colud足够(如果合适的话)在许多方面将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.
    
  • 1

    也许这是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;
                }
    
  • 1

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

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

    编辑:嗯,我想如果你在你的 class 上使用 @Table 注释那将是有用的 . 嘿 . :)

  • 10

    即使您的注释被正确声明以正确管理一对多关系,您仍可能遇到此精确异常 . 将新的子对象 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 值 .

  • 181

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

    我遇到了同样的问题, CascadeType.MERGE 对我有用 .

    我希望你排序 .

  • 0

    使用合并是冒险和棘手的,因此在您的情况下这是一个肮脏的解决方法 . 至少需要记住,当您将实体对象传递给合并时,它会停止附加到事务,而是返回一个新的,现在附加的实体 . 这意味着如果任何人仍然拥有旧的实体对象,则对它的更改会被默认忽略并在提交时被丢弃 .

    您没有在此处显示完整的代码,因此我无法仔细检查您的交易模式 . 达到这种情况的一种方法是,如果在执行合并并持久化时没有活动事务 . 在这种情况下,持久性提供程序应为您执行的每个JPA操作打开一个新事务,并在调用返回之前立即提交并关闭它 . 如果是这种情况,合并将在第一次运行事务然后在merge方法返回之后,事务完成并关闭,并且返回的实体现在已分离 . 然后,它下面的持久化将打开第二个事务,并尝试引用一个已分离的实体,并给出异常 . 除非您非常清楚自己在做什么,否则始终将代码包装在事务中 .

    使用容器管理的事务,它看起来像这样 . 请注意:这假设该方法位于会话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);
    }
    
  • 6

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

相关问题