JPA与JTA:持久化实体和合并级联子实体

21

我有一个双向的一对多关系,涉及以下实体类:

0或1个客户端 <-> 0或多个产品订单

在持久化客户端实体时,我希望相关的产品订单实体也被持久化(因为它们与“父”客户端的外键可能已经更新)。

当然,所有必需的级联选项都在客户端方面设置。但是如果在引用现有产品订单的同时首次持久化新创建的客户端时,它将无法正常工作,如下所示:

  1. 创建并持久化产品订单“1”。可以正常工作。
  2. 创建客户端“2”并将产品订单“1”添加到其产品订单列表中。然后它被持久化。无法正常工作。

我尝试了几种方法,但没有一种能够显示出预期的结果。请参见下面的结果。我阅读了这里的所有相关问题,但它们没有帮助我。我使用 EclipseLink 2.3.0、纯 JPA 2.0 注释和 JTA 作为 GlassFish 3.1.2 上基于 Apache Derby (JavaDB) 的内存数据库上的事务类型。实体关系由 JSF GUI 管理。对象级别的关系管理有效(除了持久化),我用 JUnit 测试进行了测试。

方法 1) “默认”(基于 NetBeans 类模板)

客户端:

@Entity
public class Client implements Serializable, ParentEntity {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "client", cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH},
            fetch= FetchType.LAZY)
    private List<ProductOrder> orders = new ArrayList<>();

    // other fields, getters and setters
}

产品订单:

@Entity
public class ProductOrder implements Serializable, ChildEntity {
    private static final long serialVersionUID = 1L;

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

    @ManyToOne // owning side
    private Client client;

    // other fields, getters and setters
}

通用持久化门面:

// Called when pressing "save" on the "create new..." JSF page
public void create(T entity) {
    getEntityManager().persist(entity);
}

// Called when pressing "save" on the "edit..." JSF page
public void edit(T entity) {
    getEntityManager().merge(entity);
}

结果:

create() 方法会立即抛出此异常:

警告:调用 EJB ClientFacade 方法 public void javaee6test.beans.AbstractFacade.create(java.lang.Object) 时发生系统异常 javax.ejb.EJBException: 事务已中止...

原因: javax.transaction.RollbackException: 事务标记为回滚...

原因: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLIntegrityConstraintViolationException: 这个语句将在 'PRODUCTORDER' 上引发一个唯一或主键约束条件的重复关键值。错误代码:-1 查询:INSERT INTO PRODUCTORDER (ID, CLIENT_ID) VALUES (?, ?) bind => [2 parameters bound] Query: InsertObjectQuery(javaee6test.model.ProductOrder[ id=1 ]) ...

原因: java.sql.SQLIntegrityConstraintViolationException: 这个语句将在唯一或主键约束条件或唯一索引上引发一个重复的关键值,该约束条件或唯一索引由 'SQL120513133540930' 定义在 'PRO-DUCTORDER' 上。...

原因: org.apache.derby.client.am.SqlException: 这个语句将在唯一或主键约束条件或唯一索引上引发一个重复的关键值,该约束条件或唯一索引由 'SQL120513133540930' 定义在 'PRODUCTOR-DER' 上。

我不理解这个异常。edit() 方法可以正常工作。但是我想在创建客户端时将产品订单添加到其中,因此这种方式是不够的。

方法2)仅使用 merge()

通用持久性 Facade 的更改:

// Called when pressing "save" on the "create new..." JSF page
public void create(T entity) {
    getEntityManager().merge(entity);
}

// Called when pressing "save" on the "edit..." JSF page
public void edit(T entity) {
    getEntityManager().merge(entity);
}

结果:

在 create() 方法中,EclipseLink 日志输出如下:

Fine: INSERT INTO CLIENT (ID, NAME, ADDRESS_ID) VALUES (?, ?, ?) bind => [3 parameters bound]

但是没有对产品订单表进行 "UPDATE" 操作。因此,关系没有建立。另一方面,edit() 方法运行正常。

方法三)在两种实体类型上使用 GenerationType.IDENTITY

对客户和产品订单类的更改:

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

结果:

在create()方法中,EclipseLink日志输出如下:

Fine: INSERT INTO CLIENT (NAME, ADDRESS_ID) VALUES (?, ?) bind => [2 parameters bound]

Fine: values IDENTITY_VAL_LOCAL()

Fine: INSERT INTO PRODUCTORDER (ORDERDATE, CLIENT_ID) VALUES (?, ?) bind => [2 parameters bound]

Fine: values IDENTITY_VAL_LOCAL()

因此,与添加到客户端列表的产品订单建立关系不同,会创建并持久化一个新的产品订单实体,并建立与该实体的关系。同样,在edit()方法中可以正常工作。

方法4:将方法2和3结合起来

结果:与方法2相同。

我的问题是:有没有办法实现上述描述的情况?如何实现?我想使用JPA(不使用特定于供应商的解决方案)。

4个回答

8

你好,我今天也遇到了同样的问题,我在openJPA邮件列表中提出了此问题:

您好。我遇到了一个问题,就是在同一个实体中插入和更新引用。

我正在尝试插入一个新对象(Exam),该对象引用另一个对象(Person),同时我想更新Person对象的一个属性(birthDate)。虽然我将CascadeType设置为ALL,但更新从未发生。唯一有效的方法是执行persist操作,然后进行merge操作。这正常吗?我需要更改什么吗?

我不喜欢使用Person对象中的merge进行“手动更新”,因为我不知道用户想要更新多少个对象(Exam的子对象)。

实体:

public class Exam{  
   @ManyToOne(cascade= CascadeType.ALL)
   @JoinColumn(name = "person_id")
   public Person person;
......
}

public class Person{
    private Date birthDate;
   @OneToMany(mappedBy = "person")
    private List<Exam> exams
.......
}

public class SomeClass{
   public void someMethod(){
      exam = new Exam()
      person.setBirthDate(new Date());
      exam.setPerson(person); 
      someEJB.saveExam(exam);
   }
}

public class someEJB(){

   public void saveExam(Exam exam){
        ejbContext.getUserTransaction().begin();
        em.persist(exam);
        //THIS WORKS
        em.merge(exam.getPerson());
        ejbContext.getUserTransaction().commit();       
   }

}

我需要针对每个子对象使用MERGE方法吗?

答案是:

看起来你的问题是,考试是新建的,但是人已经存在,当级联持久操作时,现有的实体被忽略了。我认为这是预期的行为。

只要你的关系被设置为CascadeType.ALL,你可以将em.persist(exam);更改为em.merge(exam);。这将负责持久化新的考试,并将合并调用级联到人。

谢谢, Rick


希望这可以帮助你。


1

@SputNick,我按照以下步骤解决了这个问题。我将使用您的代码片段来演示我所做的事情。

您有一个客户实体-@OneToMany和一个产品订单实体-@ManyToOne。我将使用

// Called when pressing "save" on the "edit..." 
public void edit(T entity) {
 getEntityManager().merge(entity);}

对于持久性,它可以为我提供灵活性来保存和更新。

要创建客户及其相应的产品订单,请使用

client.set..(some property to be saved);

productOrder.set..(some property to be saved);

//为客户设置productOrder

List<ProductOrder> order = new ArrayList<>();

client.setOrders(order); //请注意,此处需要传递一个列表

//为productOrder设置客户端

productOrder.setClient(productOrder);

.edit(client) or .edit(productOrder)

请注意,无论哪种方式,都可以使用您的更改更新两个实体表。
虽然晚了,但希望能对某些人有所帮助。

1

确保你设置了关系的两个方向,你不能只在客户端添加订单,还需要设置订单的客户端。另外,你需要合并你所改变的两个对象。如果你只合并了客户端,那么订单客户端将不会被合并(尽管你的级联可以引起合并)。

持久化操作无法工作,因为它要求被持久化的对象在持久化上下文中是正确的,即不参考分离的对象,而是参考已托管的对象。

你的问题来自于你分离对象。如果你没有分离对象,你就不会有同样的问题。通常在JPA中,你将创建一个实体管理器,找到/查询你的对象,编辑/持久化它们,然后调用提交。不需要合并。


7
我发现一个与你的建议类似的解决方法:我删除了所有“级联”选项,并通过手动维护“依赖”实体的列表,在“父”实体持久化/合并时合并它们。但是,我不会接受这个答案,因为这可能永远不会成为“官方的JPA”方法。此外,我没有显式地分离任何实体,EclipseLink日志也没有提到任何分离操作。我也不明白这些“级联”选项的作用,因为它们显然没有起作用。 - SputNick
四年后,我不得不做完全相同的事情。起初,我按照书本上的方法使用了JPA级联注释,但最终我不得不删除这些注释并持久化我的父实体,将它们添加回每个子实体中,然后使用合并操作将它们持久化。 - Douglas De Rizzo Meneghetti
2
@DouglasDeRizzoMeneghetti 因为JPA与Hibernate在一起使用非常糟糕!20年的行业经验,却没有一个直接解决级联合并问题的方案。这实在是太丢人了。 - ieXcept
你最后一段文字解决了我的问题。我从数据库中检索到了实体,然后合并了所有更改的成员。 - Philippe Gioseffi

0
请在您的ProductOrder实体中使用@Joincolumn注释,见下方。
   @Entity
   public class ProductOrder implements Serializable, ChildEntity {
   private static final long serialVersionUID = 1L;

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

   @ManyToOne // owning side
   @JoinColumn(name = "PRODUCTORDER_ID", referencedColumnName = "ID")
   //write your database column names into name and referencedColumnName attributes.
   private Client client;

   // other fields, getters and setters
   }

1
不,这样做是不起作用的。实际上,它并没有改变任何东西。我尝试使用@JoinColumn(name = "CLIENT_ID", referencedColumnName = "ID"),但它仍然不起作用。 - SputNick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接