PersistentObjectException: JPA和Hibernate抛出的传递给持久化的分离实体

我有一个包含多对一关系的jpa持久化对象模型:一个Account有多个TransactionsTransaction有一个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对象。但是,当我创建一个事务使用一个已经存在的持久化帐户并持久化事务时,我得到一个异常:

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

因此,我能够持久化包含事务的Account,但不能持久化具有Account的Transaction。我以为这是因为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 ?

631776 次浏览

可能在这种情况下,你使用归并逻辑获得了account对象,而persist用于持久化新对象,如果层次结构中有一个已经持久化的对象,它将报错。在这种情况下你应该使用saveOrUpdate,而不是persist

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

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

编辑:嗯,我想如果你在你的类上使用@Table注释,这将是有用的。哈。:)

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

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

Many端的示例setter位于这个链接。

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

也许这是OpenJPA的bug,当回滚时它重置了@Version字段,但pcVersionInit保持true。我有一个声明@Version字段的AbstraceEntity。我可以通过重置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;
}

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

我也有同样的问题,CascadeType.MERGE已经为我工作。

我希望你已经整理好了。

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

这里没有显示完整的代码,因此我无法再次检查您的事务模式。出现这种情况的一种方法是,在执行合并和持久化时没有活动的事务。在这种情况下,持久化提供程序应该为您执行的每个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到附加的数据模型时,你需要管理主键值除非你不该这么做。如果在调用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已经被管理时,不要在应用程序代码中设置id值。

如果没有任何帮助,你仍然得到这个异常,检查你的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方法中的内容

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

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.

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

cascadeType.MERGE,fetch= FetchType.LAZY

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

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

(FetchType.EAGER可以被删除,它是@ManyToOne的默认值)

这是所有!

为什么?通过在子实体Transaction上说“cascade ALL”,你要求每个DB操作都被传播到父实体Account。如果你执行persist(transaction)persist(account)也会被调用。

但只有瞬态(新)实体可以传递给persist(在本例中为Transaction)。分离的(或其他非瞬态)可能不是(在这种情况下Account,因为它已经在DB中)。

因此你会得到异常“传递给持久化的分离实体”Account实体的意思是!不是你调用persistTransaction


一般来说,你不希望从子繁殖到父。不幸的是,在书中(甚至是很好的书)和网上有许多代码示例,它们正是这样做的。我不知道,为什么……也许有时只是一遍又一遍地复制,没有多想……

如果你调用remove(transaction)在@ManyToOne中仍然有“cascade ALL”,猜猜会发生什么?account(顺便说一下,还有所有其他事务!)也将从DB中删除。但这不是你的本意,对吧?

在我的情况下,当坚持方法被使用时,我正在提交事务。 在将坚持方法更改为保存方法时,问题得到解决

如果上述解决方案不工作,只是一次注释实体类的getter和setter方法,不设置id的值。(主键)

@OneToMany(mappedBy = "xxxx", cascade={CascadeType. exe)合并,CascadeType。PERSIST, CascadeType.REMOVE})为我工作。

移除子关联级联

因此,你需要从@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);
}

我基于Spring Data jpa的答案是:我只是在外部方法中添加了@Transactional注释。

为什么它有效

由于没有活动的Hibernate Session上下文,子实体立即被分离。提供一个Spring (Data JPA)事务可以确保存在Hibernate会话。

参考:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/

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

这发生在我身上,因为我没有设置Id(这不是自动生成的)。并试图拯救@ManytoOne的关系

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

实体

@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)的值

这是我的药。

下面是我的实体。标记id是用@GeneratedValue(strategy = GenerationType.AUTO)注释的,这意味着id将由Hibernate生成。不要在创建实体对象时设置它因为它将由Hibernate自动生成。 请注意,如果实体id字段没有标记为@GeneratedValue,那么不手动为id分配值也是一种犯罪,这将受到IdentifierGenerationException:该类的id必须在调用save()之前手动分配

的欢迎
@Entity
@Data
@NamedQuery(name = "SimpleObject.findAll", query="Select s FROM SimpleObject s")
public class SimpleObject {


@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;


@Column
private String key;


@Column
private String value;


}

这是我的主类。

public class SimpleObjectMain {


public static void main(String[] args) {


System.out.println("Hello Hello From SimpleObjectMain");


SimpleObject simpleObject = new SimpleObject();
simpleObject.setId(420L); // Not right, when id is a generated value then no need to set this.
simpleObject.setKey("Friend");
simpleObject.setValue("Bani");


EntityManager entityManager = EntityManagerUtil.getEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(simpleObject);
entityManager.getTransaction().commit();


List<SimpleObject> simpleObjectList = entityManager.createNamedQuery("SimpleObject.findAll").getResultList();
for(SimpleObject simple : simpleObjectList){
System.out.println(simple);
}


entityManager.close();
        

}
}

我想救它的时候,它把它扔出去了

PersistentObjectException: detached entity passed to persist.

我所需要修复的是删除主方法中simpleObject的id设置行。

我遇到这个问题的另一个原因是事务中存在未经过Hibernate版本控制的实体。

为所有映射实体添加@Version注释

@Entity
public class Customer {


@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;


@Version
private Integer version;


@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "orders")
private CustomerOrders orders;


}
@Entity
public class CustomerOrders {


@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;


@Version
private Integer version;


private BigDecimal value;


}
这个错误来自JPA生命周期。 要解决,不需要使用特定的装饰器。只需像这样使用merge来连接实体:

entityManager.merge(transaction);

不要忘记正确设置你的getter和setter,这样你的两边都是同步的。

所以我偶然发现了这个问题和答案,因为我得到了相同的错误,但一个非常基本的对象,只有字符串和整数。

但在我的情况下,我试图将一个值设置为一个带有@Id注释的字段。

因此,如果你正在使用@Id,似乎你不能在一个类上创建一个新对象,并自己设置一个Id并将其持久化到数据库。然后,您应该将Id留空。我不知道,也许这对其他人有帮助。