296

我有一个包含多对一关系的 JPA 持久对象模型:一个Account有很多Transactions. ATransaction有一个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:分离实体传递给持久化:com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(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对象关联的 , ?

4

21 回答 21

348

解决方案很简单,只需使用CascadeType.MERGE代替CascadeType.PERSISTor CascadeType.ALL

我有同样的问题,CascadeType.MERGE并为我工作。

我希望你被排序。

于 2015-03-24T14:28:51.240 回答
145

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

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

许多方面的示例设置器在此链接中。

更正设置器后,您希望将实体访问类型声明为“属性”。声明“属性”访问类型的最佳实践是将所有注释从成员属性移动到相应的 getter。一个重要的词是不要在实体类中混合“字段”和“属性”访问类型,否则 JSR-317 规范未定义行为。

于 2012-11-21T19:45:10.423 回答
48

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

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

FetchType.EAGER可以删除,它是默认的@ManyToOne

就这样!

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

但只有瞬态(新)实体可以传递给persistTransaction在这种情况下)。分离的(或其他非瞬态状态)可能不会(Account在这种情况下,因为它已经在数据库中)。

因此,您会得到异常“传递给持久化的分离实体”Account实体的意思!不是Transaction你呼吁persist的。


您通常不希望从孩子传播到父母。不幸的是,书籍(甚至是好书)和网络中有很多代码示例,它们正是这样做的。我不知道,为什么......也许有时只是一遍又一遍地复制而不加思考......

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

于 2019-01-19T21:30:52.793 回答
31

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

于 2017-12-15T13:33:18.920 回答
24

删除子关联级联

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

@ManyToOne(fetch= FetchType.LAZY)

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

设置关联的双方

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

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

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}
于 2019-09-15T06:02:06.247 回答
15

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

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

使用容器管理的事务,它看起来像这样。请注意:这假定该方法位于会话 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);
}
于 2015-07-13T13:15:09.080 回答
14

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

于 2012-11-13T23:02:34.137 回答
6

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

为什么有效

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

参考:

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

于 2019-10-22T21:09:38.390 回答
5

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

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

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

于 2012-11-13T23:12:12.997 回答
5

如果没有任何帮助并且您仍然遇到此异常,请检查您的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方法中的内容。

于 2016-08-04T23:03:43.230 回答
5

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

实体

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

于 2021-03-10T12:45:47.637 回答
4

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

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

在这种情况下,注释声明数据库将在插入时管理实体主键值的生成。自己提供一个(例如通过 Id 的设置器)会导致此异常。

或者,但实际上相同,此注释声明导致相同的异常:

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

因此,当它已经被管理时,不要id在应用程序代码中设置它。

于 2016-06-17T13:17:34.987 回答
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;
            }
于 2014-07-05T15:54:06.277 回答
1

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

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

或者在许多方面将 ids 设置为 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.
于 2017-08-28T12:29:35.203 回答
1
cascadeType.MERGE,fetch= FetchType.LAZY
于 2018-08-23T13:46:00.250 回答
1

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

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

于 2020-03-30T11:17:28.770 回答
0

在我的情况下,当使用持久方法时,我正在提交事务。在更改持久保存方法时,它得到了解决。

于 2019-03-05T13:36:47.773 回答
0

如果上述解决方案不起作用,只需注释一次实体类的 getter 和 setter 方法,并且不要设置 id.(Primary key) 的值,那么这将起作用。

于 2019-08-22T10:08:53.260 回答
0

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

于 2019-09-01T09:26:42.413 回答
0

这是我的修复。

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

@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.

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

于 2021-06-14T03:26:38.943 回答
0

我遇到此问题的另一个原因是事务中的实体不是由 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;

}
于 2021-10-20T10:54:56.633 回答