Wouter Wouter - 2 months ago 16
Java Question

How do I avoid getting ValidationException when using a manytomany relation with jpa?

I randomly get the following exception:

org.eclipse.persistence.exceptions.ValidationException Exception Description: The attribute [id] of class [domain.Person] is mapped to a primary key column in the database. Updates are not allowed.


The exception is thrown mostly when executing "deleteAllPersons()". The error occurs when there exists a link between the 2 entities that has to be deleted, and Seems to occur on the second person that gets deleted in my case.

It is also noteworthy that the error often doesn't appear in debug mode or when the test suite is run on it's own. This leads me to believe that the problem might be timing related or that the cause is an inconsistency that in some cases gets resolved automatically before it causes any issues.

public void deleteAllPersons(){
for(Person person: getAllPersons()){
long id = person.getId();
deletePerson(id);
}


with the deletePerson(id) method being

public void deletePerson(long id) {
Person person = getPerson(id);
List<OrderBill> ordersToBeUnlinked = new ArrayList<>();
ordersToBeUnlinked.addAll(person.getOrders());

for (OrderBill order : ordersToBeUnlinked) {
person.removeOrder(order);
System.out.println(order + "deleted");
}
try{
manager.getTransaction().begin();
manager.merge(person);
} catch (Exception e1){
throw new DbException("Error merging person" + person + "\n" + e1.getMessage(), e1);
}
try {
manager.remove(person);
manager.flush();
manager.getTransaction().commit();
} catch (Exception e){
manager.getTransaction().rollback();
throw new DbException("error deleting " + person + "\n" + e.getMessage(), e);
}
}


Merging goes fine, but on deleting the exception is thrown.
In the exception message I can see that Person has no more orders in it's list

In case I made a mistake in the Person or OrderBill class,
Here is the most important part of both of those classes:

Person

@Entity(name="Person")
public class Person {
@Id
@Column(name="PERSON_ID")
@GeneratedValue
private long id;

@ManyToMany(mappedBy="authors", fetch=FetchType.LAZY, cascade={CascadeType.MERGE})
private Set<OrderBill> orders;
@Column(name="name")
private String name = "undefined";
public Person(String name){
setName(name);
payments = new HashSet<Payment>();
orders = new HashSet<OrderBill>();
}
public void removeOrder(OrderBill order){
orders.remove(order);
if(order.getAuthors().contains(this)){
order.removeAuthor(this);
}
}


OrderBill:

@Entity(name="OrderBill")
@NamedQueries({
@NamedQuery(name="Order.getAll", query="select o from OrderBill o"),
@NamedQuery(name="Order.findOrders", query="select o from OrderBill o join o.orderWeek as w where (w.weekNr = :w and w.yearNr = :y)")
})
public class OrderBill implements Transaction{
@Id
@Basic(optional = false)
@NotNull
@Column(name="ORDER_ID")
@GeneratedValue
private long id;

@ManyToMany
@JoinTable(
name="ORDER_PERSON",
joinColumns=@JoinColumn(name="ORDER_ID"),
inverseJoinColumns=@JoinColumn(name="PERSON_ID")
)
private Set<Person> authors;

public OrderBill(double totalCost, int weekNr, int yearNr){
setOrderWeek(new OrderWeek(weekNr, yearNr));
setTotalCost(totalCost);
authors = new HashSet<>();
}

public void removeAuthor(Person author){
authors.remove(author);
if(author.getOrders().contains(this))
author.removeOrder(this);
}

Answer

In case it helps someone, I'll write what I have found below:

Chris's comment turned out to be correct, but it took me a long time before I found where I made a mistake when trying to delete all references.

I don't know if this is the only problem, but I have found one main logic flaw:

  1. When I want to delete the person I first remove all it's references from the orders list and than merge the person to update the database.

But the orders list is marked "mappedBy" so it's changes won't be reflected in the database

  1. I had updated each order through "orders" list in person,so I expected the cascade merge to handle it

But by updating the person, the list with orders gets emptied. Because of this no references to the updated orders are left when the person gets merged (so the cascade merge doesn't affect them)

I fixed the problem by adding merge(order) inside the for-loop in deletePerson

for (OrderBill order : ordersToBeUnlinked) {                
    person.removeOrder(order);
    manager.merge(order);
    }

note: of course I had to put this for loop inside a transaction because of the merge.

Comments