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?
2 Answers
Answers 1
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.
Answers 2
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); }
0 comments:
Post a Comment