17

I have a Spring Boot 1.3.M1 web application using Spring Data JPA. For optimistic locking, I am doing the following:

  1. Annotate the version column in the entity: @Version private long version;. I confirmed, by looking at the database table, that this field is incrementing properly.
  2. When a user requests an entity for editing, sending the version field as well.
  3. When the user presses submit after editing, receiving the version field as a hidden field or something.
  4. Server side, fetching a fresh copy of the entity, and then updating the desired fields, along with the version field. Like this:

    User user = userRepository.findOne(id);
    user.setName(updatedUser.getName());
    user.setVersion(updatedUser.getVersion());
    userRepository.save(user);
    

I was expecting this to throw exception when the versions wouldn't match. But it doesn't. Googling, I found some posts saying that we can't set the @Vesion property of an attached entity, like I'm doing in the third statement above.

So, I am guessing that I'll have to manually check for the version mismatch and throw the exception myself. Would that be the correct way, or I am missing something?

Adrian Shum
  • 38,812
  • 10
  • 83
  • 131
Sanjay
  • 8,755
  • 7
  • 46
  • 62
  • Hibernate *does* let you modify the @version field manually (unlike, say, OpenJPA) but this is not in line with the JPA specification (see section 11.1.54 http://stackoverflow.com/questions/30098350/optimisticlockexception-not-thrown-when-version-has-changed-in-spring-boot-proje/30101542#30101542) . Your approach should work if you were binding direct to the entity. Are you passing a DTO to your service and populating the entity here? – Alan Hay Jun 17 '15 at 09:38
  • Yes, the updatedUser in my code above is the DTO. – Sanjay Jun 17 '15 at 11:47

4 Answers4

24

Unfortunately, (at least for Hibernate) changing the @Version field manually is not going to make it another "version". i.e. Optimistic concurrency checking is done against the version value retrieved when entity is read, not the version field of entity when it is updated.

e.g.

This will work

Foo foo = fooRepo.findOne(id);  // assume version is 2 here
foo.setSomeField(....);

// Assume at this point of time someone else change the record in DB, 
// and incrementing version in DB to 3

fooRepo.flush();  // forcing an update, then Optimistic Concurrency exception will be thrown

However this will not work

Foo foo = fooRepo.findOne(id);  // assume version is 2 here
foo.setSomeField(....);
foo.setVersion(1);
fooRepo.flush();  // forcing an update, no optimistic concurrency exception
                  // Coz Hibernate is "smart" enough to use the original 2 for comparison

There are some way to workaround this. The most straight-forward way is probably by implementing optimistic concurrency check by yourself. I used to have a util to do the "DTO to Model" data population and I have put that version checking logic there. Another way is to put the logic in setVersion() which, instead of really setting the version, it do the version checking:

class User {
    private int version = 0;
    //.....

    public void setVersion(int version) {
        if (this.version != version) {
            throw new YourOwnOptimisticConcurrencyException();
        }
    }

    //.....
}
Adrian Shum
  • 38,812
  • 10
  • 83
  • 131
2

You can also detach entity after reading it from db, this will lead to version check as well.

User user = userRepository.findOne(id);
userRepository.detach(user);
user.setName(updatedUser.getName());
user.setVersion(updatedUser.getVersion());
userRepository.save(user);

Spring repositories don't have detach method, you must implement it. An example:

public class BaseRepositoryImpl<T, PK extends Serializable> extends QuerydslJpaRepository<T, PK> {

   private final EntityManager entityManager;

   public BaseRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
       super(entityInformation, entityManager);
       this.entityManager = entityManager;
   }

   public void detach(T entity) {
       entityManager.detach(entity);
   }
...
}
Michal
  • 614
  • 8
  • 20
1

Part of the @AdrianShum answer is correct.

The version comparing behavior follows basically this steps:

  1. Retrieve the versioned entity with its version number, lets called V1.
  2. Suppose you modify some entity's property, then Hibernate increments the version number to V2 "in memory". It doesn't touch the database.
  3. You commit the changes or they are automatically commited by the environment, then Hibernate will try to update the entity including its version number with V2 value. The update query generated by Hibernate will modify the registry of the entity only if it match the ID and previous version number (V1).
  4. After the entity registry is successfully modified, the entity takes V2 as its actual version value.

Now suppose that between steps 1 and 3 the entity was modified by another transaction so its version number at step 3 isn't V1. Then as the version number are different the update query won't modify any registry, hibernate realize that and throw the exception.

You can simply test this behavior and check that the exception is thrown altering the version number directly on your database between steps 1 and 3.

Edit. Don't know which JPA persistence provider are you using with Spring Data JPA but for more details about optimistic locking with JPA+Hibernate I suggest you to read chapter 10, section Controlling concurrent access, of the book Java Persistence with Hibernate (Hibernate in Action)

Guillermo
  • 1,523
  • 9
  • 19
  • 6
    You missed what we are discussing. We are all clear about how optimistic concurrency works. What we are focusing is, for the update query created by Hibernate, it is NOT using the `@Version` field in the Entity. Instead, Hibernate is storing version of entity somewhere else and use that for optimistic concurrency checking. – Adrian Shum Jun 18 '15 at 10:35
0

In addition to @Adrian Shum answer, I want to show how I solved this problem. If you want to manually change a version of Entity and perform an update to cause OptimisticConcurrencyException you can simply copy Entity with all its field, thus causing an entity to leave its context (same as EntityManager.detach()). In this way, it behaves in a proper way.

Entity entityCopy = new Entity();
entityCopy.setId(id);
... //copy fields
entityCopy.setVersion(0L); //set invalid version
repository.saveAndFlush(entityCopy); //boom! OptimisticConcurrencyException

EDIT: the assembled version works, only if hibernate cache does not contain entity with the same id. This will not work:

Entity entityCopy = new Entity();
entityCopy.setId(repository.findOne(id).getId()); //instance loaded and cached 
... //copy fields
entityCopy.setVersion(0L); //will be ignored due to cache
repository.saveAndFlush(entityCopy); //no exception thrown  
Dmitry
  • 330
  • 1
  • 14
  • How do you look up a pre-existing entity but not have it in the cache to make you first code example work? – pgreen2 Dec 12 '17 at 19:02
  • @pgreen2 In this scenario you look up existing entity, but for persisting in DB you assemble it using one that is sent by a user. Detached entity's version field is modifiable then. Conversely, fetched entity's version field is not modifiable. – Dmitry Dec 14 '17 at 10:45