Blog-Archiv

Sonntag, 9. Februar 2020

Detached Objects with JPA Merge Persist Remove

This is a continuation of my recent article about JPA persist() and merge().
The behavior of these two methods and remove(), concerning detached objects, is subject to this one.

Entity States

  • Transient: Any newly constructed entity, not present in database, not contained in any persistence context. Mind that a transient entity can already have a primary key value in case the application sets it (e.g. UUID).

  • Persistent ("Managed"): The entity is present in database, i.e. it has a primary key value, and it is contained in a persistence context. This state can be achieved by EntityManager.persist(entity) or EntityManager.merge(entity), or by reading from database via EntityManager.find() or TypedQuery.getResultList() etc.

  • Detached: Such an entity is present in database, i.e. it has a primary key value, but isn't contained in any persistence context any more. This state can be achieved by EntityManager.close(), EntityManager.clear(), or EntityManager.detach(entity). Web applications that close their entity-manager after a HTTP request ended may hold lots of detached objects in their servlet session context!

  • Removed: The entity is still present in database, i.e. it still has a primary key value, and also is contained in a persistence context, but it is marked for removal. After the current transaction ended, the entity is not in database any more. This state can be achieved by EntityManager.remove(entity).

Tests

For source code of the entities, relations and methods used here, please see my recent article about persist() and merge().

All tests below are green, i.e. they succeed. Failing JPA calls have been marked by Assert.fail(), and a red left-side border.

Merge Detached

    @Test
    public void mergeSucceedsWithDetachedObject() {
        final Vehicle vehicle = JpaUtil.transactionalResult(
                entityManager, entityManager::merge, newVehicle("Mercedes"));
        entityManager.clear();
        // vehicle is saved but detached now
        
        final String NEW_NAME = "Daimler-Benz";
        vehicle.setName(NEW_NAME);
        JpaUtil.transactional(  // try to save change in detached entity
                entityManager, entityManager::merge, vehicle);            
        
        final List<Vehicle> vehicles = JpaUtil.findAll(Vehicle.class, entityManager);
        assertEquals(1, vehicles.size());
        assertEquals(NEW_NAME, vehicles.get(0).getName());
    }
    

This test constructs a new Vehicle and persist it. Then it clears the persistence context by EntityManager.clear(), we could also use EntityManager.detach(vehicle), leaving the entity detached. It modifies the detached entity and tries to save it using merge(). This succeeds. The merge() method can process detached entities, but mind that its parameter is still detached afterwards, only the return is "managed".

Persist Detached

    @Test
    public void persistFailsWithDetachedObject() {
        final Vehicle vehicle = newVehicle("Volkswagen");
        JpaUtil.transactional(
                entityManager, entityManager::persist, vehicle);
        entityManager.clear();
        // vehicle is saved but detached now
        
        final String NEW_NAME = "VW";
        vehicle.setName(NEW_NAME);
        try {
            JpaUtil.transactional(  // try to save change in detached entity
                    entityManager, entityManager::persist, vehicle);
            
            fail("Persist of detached object must fail!");
        }
        catch (Exception e) {
            // exception is expected here
            System.err.println(toDeepest(e).toString());
        }
    }

The same as above, but this time we use persist() to save the detached entity. The fail() asserts that such an attempt fails, although it would be nice if it succeeded:-) But that's how JPA works. The persist() method can not handle detached entities. Following are the error messages of the JPA providers:

  • Hibernate: PersistentObjectException: detached entity passed to persist: ...
  • EclipseLink: (fails with an database-specific SQL error message about primary key constraint violation on INSERT)

Once again a difference between the JPA providers: EclipseLink doesn't detect the detached state but tries to insert the entity as new.

Remove Detached

    @Test
    public void removeFailsWithDetachedObject() {
        final Vehicle vehicle = newVehicle("Cadillac");
        JpaUtil.transactional(
                entityManager, entityManager::persist, vehicle);
        entityManager.clear();
        // vehicle is saved but detached now
        
        try {
            JpaUtil.transactional(  // try to remove detached entity
                    entityManager, entityManager::remove, vehicle);
            
            fail("Remove of detached object must fail!");
        }
        catch (Exception e) {
            // exception is expected here
            System.err.println(toDeepest(e).toString());
        }
    }

This is the same case as persist(), the remove() method can not handle detached objects. Following are the error messages:

  • Hibernate: IllegalArgumentException: Removing a detached instance ....
  • EclipseLink: IllegalArgumentException: Entity must be managed to call remove: ....

Conclusion

We have EntityManager.contains(entity) to find out whether an entity is "managed", but we can't distinguish transient and detached states generically, because sometimes primary keys are evaluated by the application on entity-construction, sometimes by the JPA layer from database sequences on saving the entity.

When you merge() detached objects, be aware that not the object itself changes state then, it is the returned clone only that is "managed" afterwards!

Concerning detached objects, the merge() method shines. You can't update from a detached instance with persist().




Keine Kommentare: