Blog-Archiv

Samstag, 8. Februar 2020

JPA Persist and Merge with Unsaved Objects on Different Providers

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: