Optimistic locking not throwing exception when manually setting version field

前端 未结 4 475
太阳男子
太阳男子 2020-12-08 17:03

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:
相关标签:
4条回答
  • 2020-12-08 17:20

    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();
            }
        }
    
        //.....
    }
    
    0 讨论(0)
  • 2020-12-08 17:26

    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);
       }
    ...
    }
    
    0 讨论(0)
  • 2020-12-08 17:30

    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)

    0 讨论(0)
  • 2020-12-08 17:46

    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  
    
    0 讨论(0)
提交回复
热议问题