Question

In my current project I use Spring Data JPA with Hibernate but consider this as a more general question that should also cover "plain" JPA.

I'm uncertain how I should deal with OptimisticLockException when using @Version.

Due to how my application works some relationships have CascadeType.PERSIST and CascadeType.REFRESH, others also have CascadeType.MERGE.

  1. Where to handle OptimisticLockException

As far as I can tell handling this on the service layer won't work especially with CascadeType.MERGE because then the offending entity could be one that needs to be handled by another service ( I have a service per entity class).

Problem is I'm creating a framework and hence there is no layer above service so I could just "delegate" this to the user of my framework but that seems "weak and lazy".

  1. Determine offending entity and fields that changed

if a OptimisticLockException occurs, how do I get which entity caused the issue and what fields were changed?

Yes, i can call getEntity() but how do I cast that to the correct Type especially if CascadeType.MERGE was used? The entity could be of multiple types, so a if/switch with instanceof comes to mind but this seems ugly like hell.

Once I have the correct type I would need to get all the differences between the versions excluding certain fields like version itself or lastModifiedDate.

In the back of my mind is also HTTP 409 which stated that in case of conflict response should contain the conflicting fields.

Is there a "best-practice-pattern" for all this?

Was it helpful?

Solution 2

What was bothering me is that the exceptions provided by JPA (Hibernate) and Spring do not actually return the current version of the failed object. So if a User needs to decide what to do, he obviously needs to see the updated, most current version. Just retarded an error to his call seems retarded to me. I mean you are already at database level in a transaction so getting the new current value directly has no cost...

I created a new Exception that holds a reference to the newest version of the entity that failed to update:

public class EntityVersionConflictException {

    @Getter
    private final Object currentVersion;

    public EntityVersionConflictException(
            ObjectOptimisticLockingFailureException lockEx,
            Object currentVersion){
        super(lockEx);
        this.currentVersion = currentVersion;
    }

    public Object getConflictingVersion() {
        return ((OptimisticLockException)getCause().getCause()).getEntity();
    }

    public Class getEntityClass() {
        return getCause().getPersistentClass();
    }

    @Override
    public ObjectOptimisticLockingFailureException getCause(){
        return (ObjectOptimisticLockingFailureException)super.getCause();
    }
}

and the according Service method

try {
    return getRepository().save(entity);
} catch (ObjectOptimisticLockingFailureException lockEx) {
    // should only happen when updating existing entity (eg. merging)
    // and because entites do not use CascadeType.MERGE
    // the entity causing the issue will always be the of class
    // entity.getClass()
    // NOTE: for some reason lockEx.getPersistentClass() returns null!!!
    // hence comparing by class name...
    if (lockEx.getPersistentClassName().equals(entityClass.getName())) {
        T currentVersion = getById(entity.getId());
        throw new EntityVersionConflictException(lockEx, currentVersion);
    } else {
        throw lockEx;
    }
}

Note the comments. In case of CascadeType.MERGE this will not work like this, the logic would have to be much more complex. I have 1 service per entity type so that service would have to hold reference to all other services and so forth.

OTHER TIPS

The whole point of optimistic locking is to be able to tell the end user: Hey, you tried to save this important piece of information, but someone else saved it behind your back, so you'd better refresh the information, decide if you still want to save it and potentially enter some new values, and then retry.

Just like with SVN, if you try to commit a file and someone else committed a new version before, SVN forces you to update your working copy and resole potential conflicts.

So I would do the same as what JPA does : it lets the caller decide what to do by throwing the exception. This exception should be handled in the presentation layer.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top