JPA Persist and Merge with Unsaved Objects on Different Providers


Published: 2020-02-08
Updated: 2020-02-15
Web: https://fritzthecat-blog.blogspot.com/2020/02/jpa-persist-and-merge-with-unsaved.html


This is the continuation of my recent article about JPA persist() and merge().
Let's see how the two major JPA providers behave when you try to save a relation that contains unsaved (transient) objects.

Test Code

For entities, relations and methods used here, please see my recent article. Here is an additional helper that needs to be in the unit test:



private Throwable toDeepest(Throwable e) {
while (e.getCause() != null && e != e.getCause())
e = e.getCause();
return e;
}

This loops down to the real cause of an exception and returns it. Useful for error messages.

Saving by persist()

Unsaved NOT NULL Relation

Following test builds together a graph containing a Vehicle, a Workshop, and a Repair on that vehicle, performed by that workshop. None of the entities is persistent. Repair does not cascade to Workshop, but Repair requires a relation to Workshop (NOT NULL). By saving the cascading Vehicle, all entities should be persisted except Workshop, and that should cause an exception.

    @Test
public void persistWithUnsavedNotNullRelation() {
final Vehicle vehicle = newVehicle("Bentley");
final Workshop unsavedRelation = newWorkshop("Jill's Fast Shop");
newRepair("Breaks", vehicle, unsavedRelation);
try {
JpaUtil.transactional(entityManager, entityManager::persist, vehicle);

final int workshops = JpaUtil.findAll(Workshop.class, entityManager).size();
fail("Persist with an unsaved not-null relation must not work! Number of persistent workshops: "+workshops);
}
catch (Exception e) {
// exception is expected here
System.err.println(toDeepest(e).toString());
}
}

The resulting messages of the (correct and expected) exceptions are:

So far so good, both JPA providers work as expected. Let's see how they do on unsaved nullable relations.

Unsaved Nullable Relation

Again we use a graph containing a Vehicle, a Workshop, and a Repair on that vehicle, performed by that workshop. Difference is that Workshop gets persisted, thus causes no exception any more. The optional relation to Agent now holds the unsaved object. Repair does not cascade to Agent.

    @Test
public void persistWithUnsavedNullableRelation() {
final Workshop shop = newWorkshop("John's Repair Shop");
JpaUtil.transactional(entityManager, entityManager::persist, shop);

final Vehicle vehicle = newVehicle("Porsche");
final Repair repair = newRepair("Seats", vehicle, shop);
final Agent unsavedRelation = newAgent("Maxwell Hammer");
repair.setAgent(unsavedRelation);
try {
JpaUtil.transactional(entityManager, entityManager::persist, vehicle);

final int agents = JpaUtil.findAll(Agent.class, entityManager).size();
fail("Persist with an unsaved nullable relation must not work! Number of persistent agents: "+agents);
}
catch (Exception e) {
// exception is expected here
System.err.println(toDeepest(e).toString());
}
}

This actually looks good. Now let's try the same with merge().

Saving by merge()

Unsaved NOT NULL Relation

The same as the first test above, but using merge() instead of persist().

    @Test
public void mergeWithUnsavedNotNullRelation() {
final Vehicle vehicle = newVehicle("Ferrari");
final Workshop unsavedRelation = newWorkshop("Suzie's Superstore");
newRepair("Tires", vehicle, unsavedRelation);
try {
JpaUtil.transactionalResult(entityManager, entityManager::merge, vehicle);

// EclipseLink saves Workshop!
final int workshops = JpaUtil.findAll(Workshop.class, entityManager).size();
fail("Merge with an unsaved not-null relation must not work! Number of persistent workshops: "+workshops);
}
catch (Exception e) {
// exception is expected here
System.err.println(toDeepest(e).toString());
}
}

With Hibernate, the same happened as when using persist(). But EclipseLink did NOT throw an exception, thus the test failed. Instead it persisted the non-cascaded Workshop, which resulted in the "Number of persistent workshops: 1" message.

Mind that EclipseLink is the JPA reference implementation, thus it seems to be Hibernate that failed, and the test is wrong!

Unsaved Nullable Relation

The same as the second test above, but using merge().

    @Test
public void mergeWithUnsavedNullableRelation() {
final Workshop shop = newWorkshop("Jeff's Workshop");
final Workshop mergedShop = JpaUtil.transactionalResult(entityManager, entityManager::merge, shop);

final Vehicle vehicle = newVehicle("Mustang");
final Repair repair = newRepair("Doors", vehicle, mergedShop);
final Agent unsavedRelation = newAgent("Nigel Nail");
repair.setAgent(unsavedRelation);
try {
JpaUtil.transactionalResult(entityManager, entityManager::merge, vehicle);

final int agents = JpaUtil.findAll(Agent.class, entityManager).size();
fail("Merge with an unsaved nullable relation must not work! Number of persistent agents: "+agents);
}
catch (Exception e) {
// exception is expected here
System.err.println(toDeepest(e).toString());
}
}

Again Hibernate did the same as with persist(), but EclipseLink saved the unsaved Agent as new entity, thus the test failed.

Conclusion

JPA is a big and complex specification. Providers always take their liberties. EclipseLink is the JPA reference implementation. Hibernate is much more in use. When your JPA-based software uses merge(), switching from Hibernate to EclipseLink may cause troubles.





ɔ⃝ Fritz Ritzberger, 2020-02-08