I read some articles about hibernate versioning: https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/transactions-optimistic.html https://vladmihalcea.com/jpa-entity-version-property-hibernate/
and come up with new questions about it.
For example:
class Test {
String name;
int version;
}
For this Test class, on update operation. To check version, I can follow different strategies:
First one is I can manually check versions.
update(Test request) {
Test exist = repository.find(...);
if (exist.version != request.version) throw Exception();
...
}
Second one is I can directly send version to db and optimistic-lock handles it.
update(Test request) {
Test exist = repository.find(...);
exist.setName(request.name);
exist.setVersion(request.version);
repository.update(exist);
}
and if versions don't match, it throws optimistic-lock-exception
.
When I think this case, first option looks like performant because detected problem before db-call
but there is extra if-control
. When I think an application, if there is 1M
request, optimistic lock problem might be ~%1
of requests so performance differences might be ignored.
What is your suggestion? Which one do you prefer or which one do you use on professional life?
From JPA perspective you must perform version checking every time external data gets merged into persistent context, and I do see following three correct patterns there (all other patters are either the same or wrong):
void update(Dto dto) {
// tx boundary start
Entity e = em.find(EntityType.class, dto.getId());
if (!Objects.equals(dto.getVersion(), e.getVersion())) {
throw new VersionMismatchException();
}
copyData(Dto dto, Entity e);
// tx boundary end
}
EntityManager.merge()
, HBN will perform version checking by its own.void update(Entity detached) {
// tx boundary start
Entity e = em.merge(detached);
// tx boundary end
}
EntityManager.merge()
we prefer to copy data on our own - in this case we need to check version explicitly as well as in DTO case.void update(Entity detached) {
// tx boundary start
Entity e = em.find(EntityType.class, detached.getId());
if (!Objects.equals(detached.getVersion(), e.getVersion())) {
throw new VersionMismatchException();
}
copyData(Entity detached, Entity e);
// tx boundary end
}
From spring-data-jpa
perspective we may reword previous "you must perform version checking every time external data gets merged into persistent context" as "you must perform version checking every time external data crosses transaction boundary", and the corresponding correct scenarios are following:
void update(Dto dto) {
Entity e = repository.findById(dto.getId());
if (!Objects.equals(dto.getVersion(), e.getVersion())) {
throw new VersionMismatchException();
}
copyData(Dto dto, Entity e);
repository.save(e);
}
repository.save()
void update(Entity detached) {
Entity e = repository.save(detached);
}
repository.save()
we prefer to copy data on our own - in this case we need to check version explicitly as well as in DTO case.void update(Entity detached) {
Entity e = repository.findById(dto.getId());
if (!Objects.equals(detached.getVersion(), e.getVersion())) {
throw new VersionMismatchException();
}
copyData(Entity detached, Entity e);
repository.save(e);
}
As regards to performance...
Optimistic locking is slow by design - you are forcing the client to retrieve actual version of data prior updating it, that looks natural for browser/user UI when users update what they see, however in case when our client is another system it might not look performant in some cases. In any case the attempt to measure performance in terms of the amount of if
statements is definitely bad idea.