JPA with JTA: Persist entity and merge cascaded child entities
Asked Answered
R

4

21

I have a bidirectional one-to-many relationship with the following entity classes:

0 or 1 client <-> 0 or more product orders

When persisting the client entity I want the associated product order entities to be persisted, too (as their foreign key to the "parent" client may have been updated).

Of course all required CASCADE options are set on the client side. But it does not work if a newly created client is persisted for the first time while referencing an existing product order as in this scenario:

  1. product order '1' is created and persisted. Works fine.
  2. client '2' is created and product order '1' is added to its product orders list. Then it is persisted. Does not work.

I tried several apporaches, but none of them showed the expected result. See those results below. I read all related questions here, but they didn't help me. I use EclipseLink 2.3.0, pure JPA 2.0 Annotations, and JTA as transaction type on an Apache Derby (JavaDB) in-memory DB on GlassFish 3.1.2. Entity relationships are managed by a JSF GUI. Object level relationship management works (apart from persisting), I tested it with JUnit tests.

Approach 1) "Default" (based on NetBeans class templates)

Client:

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

ProductOrder:

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

Generic Persistence Facade:

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

Result:

create() throws this Exception immediatly:

Warning: A system exception occurred during an invocation on EJB ClientFacade method public void javaee6test.beans.AbstractFacade.create(java.lang.Object) javax.ejb.EJBException: Transaction aborted ...

Caused by: javax.transaction.RollbackException: Transaction marked for rollback. ...

Caused by: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLIntegrityConstraintViolationException: The state-ment was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL120513133540930' defined on 'PRODUCTORDER'. Error Code: -1 Call: INSERT INTO PRODUCTORDER (ID, CLIENT_ID) VALUES (?, ?) bind => [2 parameters bound] Query: InsertObjectQuery(javaee6test.model.ProductOrder[ id=1 ]) ...

Caused by: java.sql.SQLIntegrityConstraintViolationException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL120513133540930' defined on 'PRO-DUCTORDER'. ...

Caused by: org.apache.derby.client.am.SqlException: The statement was aborted be-cause it would have caused a duplicate key value in a unique or primary key con-straint or unique index identified by 'SQL120513133540930' defined on 'PRODUCTOR-DER'.

I don't understand this exception. edit() works fine. BUT I would like to add product orders to a client at its creation time, so this is insufficient.

Approach 2) merge() only

Changes to Generic Persistence 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);
}

Result:

On create(), the EclipseLink Logging output says:

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

but NO "UPDATE" on the product order table. Thus, the relationship is not established. Again, edit(), on the other hand, works fine.

Apporach 3) Id GenerationType.IDENTITY on both entity types

Changes to both client and product order class:

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

Result:

On create(), the EclipseLink Logging output says:

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

thus instead of estabilshing a relationship to the product order added to the client's list, a new prodcut order entity is created and persisted (!) and a relationship to that entity is estabilshed. Same here, edit() works fine.

Apporach 4) Approach (2) and (3) combined

Result: Same as Approach (2).

My question is: Is there any way to realize the scenario described above? How can it be archieved? I'd like to stay with JPA (no vendor-specific solution).

Reach answered 14/6, 2012 at 8:22 Comment(0)
G
8

Hi i had the same problem today, i ask to the openJPA mailing list with this email:

Hi. I have a problem with insert and updating a reference in the same entity.

Im trying to insert a new object (Exam) that has a reference to another object (Person) and at the same time i want to update an attribute (birthDate) of the Person object. The update never happens although i set CascadeType to ALL. The only way this works is doing a persist and after that a merge operation. Is this normal? Do i have to change something??

I dont like the idea of a "manual update" using merge in the Person object because i don't know how many objects (child object of Exam) the user want to update.

Entities:

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

}

Do i have to use the MERGE method for every child object?

And the answer was this:

It looks like your problem is that the Exam is new, yet the Person is existing and the existing Entity gets ignored when cascading the persist operation. I believe this is working as expected.

As long as your relationships are set to CascadeType.ALL, you could always change your em.persist(exam); to em.merge(exam);. That would take care of persisting the new exam, and it would also cascade the merge call to the person.

Thanks, Rick


I hope this can help you.

Gnathous answered 8/4, 2013 at 17:44 Comment(0)
S
1

Ensure you are setting both sides of the relationship, you cannot just add the order to the client, you also need to set the client of the order. Also you need to merge both objects that you have changed. If you just merge the client, then the order's client will not be merged (although your cascade you cause it to be merged).

persist does not work, as persist requires that the object being persisted be correct for the persistence context, i.e. not reference detached objects, it must reference managed objects.

Your issue comes from you detaching of the objects. If you did not detach the objects, you would not have the same issues. Normally in JPA you will create an EntityManager, find/query your objects, edit/persist them, call commit. No merging is required.

Stereochrome answered 14/6, 2012 at 14:37 Comment(4)
I found a workaround similar to your advices: I removed any cascade options, and I persisted / merged "depending" entities manually, i.e. by maintaining a list of "depending" entities and merging them when the "parent" is persisted / merged. BUT I will not accept this answer as this may never be the "official JPA" apporach. Also I do not detach any entity explicitly, and EclipseLink logging doesn't mention any detach operation. I also don't understand what these cascade options are for, as they obviously don't work.Reach
4 years later and I had to do the exact same thing. At first, I followed the by-the-book approach of using JPA-only cascade annotations, but I ended up having to remove the annotations and persisting my parent entities, addind them back to each child entity and them persisting them using merge.Beckwith
@DouglasDeRizzoMeneghetti because JPA all together with the Hibernate sucks! 20 years in the industry and yet there is no straightforward solution to CASCADE MERGE problem. It's simply a shame.Ciapha
Your last paragraph did the trick for me. I retrieved from the database the entity and then merged with all the members changed.Lupe
S
1

@SputNick ,I solved it by the steps described below.I am going to use your code snippet to demonstrate what I did,

You have Client entity -@OneToMany and ProductOrder entity -@ManyToOne .I will use

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

for persistence as it can give me flexibility to save as well as update.

To create Client and corresponding Product orders use

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

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

//Set productOrder for the client

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

client.setOrders(order); //Note that this expects a list to be passed

//set client for the productOrder

productOrder.setClient(productOrder);

.edit(client) or .edit(productOrder)

Note that either way will be able to update both entity tables with your changes.

Though late,hope it will help someone

Slither answered 13/4, 2018 at 13:39 Comment(0)
C
0

Use @Joincolumn annotation in your ProductOrder entity, please see below.

   @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
   }
Cyzicus answered 14/6, 2012 at 10:34 Comment(1)
No, this does not work. It actually does not change anything. I tried it with @JoinColumn(name = "CLIENT_ID", referencedColumnName = "ID") , but it did not work.Reach

© 2022 - 2024 — McMap. All rights reserved.