PersistentObjectException:JPA和Hibernate传递以持久保留的分离实体


237

我有一个JPA持久对象模型,其中包含多对一关系:一个Account具有许多Transactions。A 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并持久化该Transaction,我得到一个异常:

由以下原因引起:org.hibernate.PersistentObjectException:传递给持久对象的分离实体:org.hibernate.event.internal.DefaultPersistEventListener.onPersist上的com.paulsanwald.Account(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);

如何正确保存Transaction与已经存在的Account对象关联的?


15
就我而言,我正在设置一个实体的ID,该实体试图使用Entity Manager进行持久化。当我删除I​​D的二传手时,它开始正常工作。
Rushi Shah

在我的情况下,我没有设置ID,但是有两个用户使用相同的帐户,其中一个保留了一个实体(正确),当第二个用户尝试保留相同的实体时发生了错误坚持。
sergioFC

Answers:


129

这是一个典型的双向一致性问题。在此链接以及此链接中都进行了很好的讨论

根据前面2个链接中的文章,您需要在双向关系的两侧修复设置器。此链接中有一个用于一侧的设置器示例

此链接中提供了很多方面的示例设置器

在更正了设置者之后,您想要将实体访问类型声明为“属性”。声明“属性”访问类型的最佳实践是将所有注释从成员属性移至相应的getter。一个大的警告是不要在实体类中混合使用“字段”和“属性”访问类型,否则JSR-317规范未定义其行为。


2
ps:@Id注释是hibernate用来标识访问类型的注释。
迭戈·普伦茨

2
唯一的例外是:detached entity passed to persist为什么提高一致性会使它起作用?好的,一致性已修复,但对象仍然分离。
Gilgamesz

1
@Sam,非常感谢您的解释。但是,我还是不明白。我看到没有“特殊”设置器,双向关系就无法令人满意。但是,我不明白为什么对象被分离。
Gilgamesz

28
请不要发布仅通过链接回答的答案。
El Mac

6
根本看不到这个答案与问题有什么关系?
Eugen Labun '19

270

解决方案很简单,只需使用CascadeType.MERGE代替CascadeType.PERSIST或即可CascadeType.ALL

我遇到了同样的问题,CascadeType.MERGE并为我工作。

我希望你能排序。


5
令人惊讶的是,有人也为我工作。由于CascadeType.ALL包括所有其他层叠类型... WTF,这没有任何意义。我有spring 4.0.4,spring数据jpa 1.8.0和hibernate4.X。
Vadim Kirilchuk

22
@VadimKirilchuk这也为我工作,这是很有意义的。由于“交易”是永久性的,因此它也会尝试使用PERSIST帐户,并且该帐户已存在于数据库中,因此该操作无效。但是,使用CascadeType.MERGE会自动合并帐户。
Gunslinger

4
如果您不使用事务,则会发生这种情况。
lanoxx

1
另一个解决方案:尝试不插入已经存在的对象:)
Andrii Plotnikov

3
谢谢你,兄弟。如果您将引用键限制为NOT NULL,则无法避免插入持久对象。因此,这是唯一的解决方案。再次感谢你。
makkasi

22

从子实体中删除级联Transaction,它应该是:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

FetchType.EAGER可以删除,也可以将其删除@ManyToOne

就这样!

为什么?通过在子实体上说“ cascade ALL”,Transaction您需要将每个数据库操作传播到父实体Account。如果您这样做persist(transaction)persist(account)也会被调用。

但是只有临时(新)实体可以传递给persistTransaction在这种情况下)。分离(或其他非暂态)状态可能不会(Account在这种情况下,因为它已经存在于DB中)。

因此,您将获得异常“已将分离的实体传递给持久对象”。该Account实体的意思!不是Transaction你打电话persist


您通常不希望从孩子传播到父母。不幸的是,书中有很多代码示例(即使是很好的示例),也有通过网络编写的,完全可以做到这一点。我不知道为什么?也许有时候只是一遍又一遍地复制而没有太多的思考...

猜猜如果您remove(transaction)在@ManyToOne中仍称“层叠全部”会发生什么?的account(顺便说一句,与所有其他交易!)将被从数据库以及删除。但这不是您的意图,对吗?


只是要添加,如果您的意图确实是要与父级一起保存子级,并且还要与子级一起删除父级,例如person(parent)和address(child)以及由DB自动生成的addressId,那么在调用Person上的save之前,只需在您的交易方式中保存地址的调用。这样,它将与DB生成的ID一起保存。Hibenate仍然会进行2个查询,因此对性能没有影响,我们只是在更改查询顺序。
Vikky

如果我们什么都不能传递,那么在所有情况下它将采用什么默认值。
Dhwanil Patel

15

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

您没有在此处显示完整的代码,因此我无法再次检查您的交易模式。解决这种情况的一种方法是,在执行合并和持久化操作时没有活动的事务。在这种情况下,预计持久性提供程序将为您执行的每个JPA操作打开一个新事务,并在调用返回之前立即提交并关闭它。如果是这种情况,合并将在第一个事务中运行,然后在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)。在业务层,还是即时得到同样的问题,
塞特希SP的Arumugam

我不能说我完全理解为什么事情会以这种方式工作,但是将persistent方法和视图到Entity映射一起放在Transactional注释中解决了我的问题,非常感谢。
Deadron

实际上,这是最不赞成的解决方案。
Aleksei Maide '18 -10-1

13

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


3
它是JPA,所以我认为类似的方法是.merge(),但这给了我同样的例外。需要明确的是,Transaction是一个新对象,Account不是。
保罗·桑瓦尔德

@PaulSanwald mergetransaction对象上使用您会遇到相同的错误?
2012年

实际上,不,我说错了。如果我.merge(transaction),则交易根本不会持久。
保罗·桑瓦尔德

@PaulSanwald Hmm,您确定transaction没有持续吗?您如何检查。请注意,这merge将返回对持久对象的引用。
2012年

.merge()返回的对象的ID为null。另外,我之后还要执行.findAll(),但我的对象不存在。
保罗·桑瓦尔德

12

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


2
好建议!但仅当生成id时。如果已分配,则设置ID是正常的。
Aldian

这对我有用。同样,您可以使用TestEntityManager.persistAndFlush()以使实例成为托管和持久的,然后将持久性上下文同步到基础数据库。返回原始源实体
Anyul Rivas,

7

由于这是一个非常常见的问题,因此我写了这篇文章,此答案基于该文章

为了解决此问题,您需要按照以下步骤操作:

1.删​​除儿童协会级联

因此,您需要@CascadeType.ALL@ManyToOne关联中删除。子实体不应与父级关联。仅父实体应级联到子实体。

@ManyToOne(fetch= FetchType.LAZY)

请注意,我将fetch属性设置为,FetchType.LAZY因为渴望获取对性能非常不利

2.设置关联双方

每当有双向关联时,都需要使用父实体中的addChildremoveChild方法同步双方:

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

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

有关同步双向关联的两端为何如此重要的更多详细信息,请参阅本文


4

在您的实体定义中,您没有为联接指定@JoinColumn。您会想要这样的东西:AccountTransaction

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

编辑:好吧,我想如果您@Table在课堂上使用注释会很有用。嘿。:)


2
是的,我不认为是这样,都一样,我添加了@JoinColumn(name =“ fromAccount_id”,referencedColumnName =“ id”),但是它没有用:)。
保罗·桑瓦尔德

是的,我通常不使用映射xml文件将实体映射到表,因此我通常假定其基于注释。但是,如果我不得不猜测,您正在使用hibernate.xml将实体映射到表,对吗?
NemesisX00 2012年

不,我使用的是Spring数据JPA,因此全部基于注释。我在另一侧具有“ mappedBy”注释:@OneToMany(级联= {CascadeType.ALL},fetch = FetchType.EAGER,mappedBy =“ fromAccount”)
Paul Sanwald,2012年

4

即使正确声明了批注以正确管理一对多关系,您仍可能会遇到此精确异常。在将新的子对象添加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在已经对其进行管理的应用程序代码中设置该值。


4

如果没有任何帮助,而您仍然遇到此异常,请检查您的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()支票中删除了交易。这是因为休眠意味着您不打算更新旧对象,而是在更改子集合上的元素时将新对象传递给持久对象。
当然,此解决方案并不适合所有应用程序,您应该仔细设计要包含在equalshashCode方法中的内容。


1

也许这是OpenJPA的bug,回滚时会重置@Version字段,但pcVersionInit保持为true。我有一个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

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

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.



0

就我而言,当我使用persist方法时,我正在提交事务。在不断变化的坚持保存方法,它得到了解决。


0

如果以上解决方案不起作用,只需一次注释实体类的getter和setter方法,并且不设置id的值。(主键)然后将起作用。


0

@OneToMany(mappedBy =“ xxxx”,级联= {CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REMOVE})为我工作。


0

通过在下一个对象之前保存从属对象来解决。

这是发生在我身上的,因为我没有设置ID(它不是自动生成的)。并尝试与@ManytoOne建立关联

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.