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:
- Hibernate: Not-null property references a transient value - ...
- EclipseLink: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: ....
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()); } }
- Hibernate: object references an unsaved transient instance - ...
- EclipseLink: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: ....
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()); } }
- Hibernate: Not-null property references a transient value - ...
- EclipseLink: java.lang.AssertionError: Merge with an unsaved not-null relation must not work! Number of persistent workshops: 1
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()); } }
- Hibernate: object references an unsaved transient instance - ...
- EclipseLink: java.lang.AssertionError: Merge with an unsaved nullable relation must not work! Number of persistent agents: 1
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.
Keine Kommentare:
Kommentar veröffentlichen