8

There's an entity Foo with a @Version column. If I want to delete it I expect Spring Data JPA and/or Hibernate to check whether the current value of the @Version column matches the one in the database. If it does not, the deletion should be rejected. This works as expected with a detached entity:

@Transactional
public void delete(Foo foo) {
    fooRepository.delete(foo); // throws ObjectOptimisticLockingFailureException
}

But if I load the entity first from the repository and then delete it within the same transaction using a different version the deletion passes regardless of the value of @Version column:

@Transactional
public void delete(int fooId, long version) {
    Foo foo = fooRepository.findOne(fooId);
    foo.setVersion(version);
    fooRepository.delete(foo); // passes regardless of value of version
}

When I look into the Hibernate debug output, the version comparison is performed (delete from foo where id=? and version=?) but not with the effect I'm expecting.

What am I missing?

Dragan Bozanovic
  • 23,102
  • 5
  • 43
  • 110
aha
  • 3,702
  • 3
  • 38
  • 47

2 Answers2

11

From the JPA specification, section 3.4.2:

An entity may access the state of its version field or property or export a method for use by the application to access the version, but must not modify the version value. With the exception noted in section 4.10, only the persistence provider is permitted to set or update the value of the version attribute in the object.

The purpose of the version property is to guard us from concurrent updates that may happen after the object is loaded in the current persistence context, and Hibernate implements it by ignoring any value you set manually, but rather uses the value obtained from the database when the object is loaded. To verify this, enable printing of bound variable values as well and you will notice that the value from the database is used.

For example, the standard solution that is used in practice when working with DTOs is to perform the check manually when updating entity state from DTOs:

if (entity.getVersion() != dto.getVersion()) {
    throw new OptimisticLockException("...");
}

Of course you can make this more generic by extending from a base class that provides this check for all version-able entities, or in some util method. For example, some authors do it in the version setter directly:

public void setVersion(long version) {
    if (this.version != version) {
      throw new OptimisticLockException("...");
    }
} 

Hibernate performs this check automatically for detached entities, as can be seen in the implementation of DefaultMergeEventListener:

else if (isVersionChanged(entity, source, persister, target)) {
    if (source.getFactory().getStatistics().isStatisticsEnabled()) {
        source.getFactory().getStatisticsImplementor()
            .optimisticFailure(entityName);
    }
    throw new StaleObjectStateException(entityName, id);
}
Community
  • 1
  • 1
Dragan Bozanovic
  • 23,102
  • 5
  • 43
  • 110
7

According to the JPA spec (section 11.1.54, emphasis mine):

The Version annotation specifies the version field or property of an entity class that serves as its optimistic lock value. The version is used to ensure integrity when performing the merge operation and for optimistic concurrency control.

Executing the repository delete operation on an unmanaged instance performs a merge first and therefore an ObjectOptimisticLockingFailureException is thrown as expected.

Executing the repository delete operation on a managed instance however directly invokes delete on the underlying EntityManager, hence no exception.


In summary, the spec requires the @Version field to be used by merge, which is not called for a managed instance, hence no error in the second case.

manish
  • 19,695
  • 5
  • 67
  • 91
  • 1
    Exactly, you can only remove managed entities, so a merge is required which is where the version check is performed. I looked at the Hibernate source, and if you looks inside `DefaultDeleteEventListener.onDelete()` you can see that it doesn't look at the version when invoking the internal delete method. – Klaus Groenbaek May 06 '17 at 12:56
  • @KlausGroenbaek, I didn't understand your comment. The poster has asked why a version mismatch leads to an error in one case and not another and I have pointed them to the spec that specifies that the version check is required in only one of the cases (in which it works) and not the other so the observed behaviour is as per the spec. When you say that Hibernate `onDelete` does not look at the version, that is expected as per the spec which does not require `delete` to examine the version. I do not see any deviation from the spec. – manish May 06 '17 at 16:14
  • I up-woted your answer because it is exactly on point. I was just pointing out if he read the code, he could see that the version is not check when deleting. Many developers ask question and wait for others to answer them, often is is faster to read/debug the code. In this case it took me less than 15 minutes to find. Of cause when it comes to JPA, it is also good to know the Spec, but sometimes you need to look at the code/docs because the vendor didn't actually implement the spec. As an example the equivalent of `LazyInitializationException` is never encountered in EclipseLink. – Klaus Groenbaek May 06 '17 at 17:09