Search code examples
hibernate

Hibernate Version Checking On Update Operation


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?


Solution

  • 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):

    1. external data is a DTO - we need to check version explicitly:
    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
    }
    
    1. external data is a detached entity - we do not need to to anything but just call 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
    }
    
    1. external data is a detached entity, but instead of calling 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:

    1. external data is a DTO - we need to check version explicitly:
    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);
    }
    
    1. external data is a detached entity - we do not need to to anything but just call repository.save()
    void update(Entity detached) {
      Entity e = repository.save(detached);
    }
    
    1. external data is a detached entity, but instead of calling 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.