spring-boothibernatespring-data-jpaspring-dataoptimistic-locking

Optimistic locking across 2 different Hibernate Sessions


I have written an update method inside the service layer like this, I am using Spring-data JpaRepository for all the CRUD operations.

    public Note updateNote(Note note, Long id) {
        Note current = repository.getOne(id);

        current.setTitle(note.getTitle());
        current.setDetail(note.getDetail());

        return repository.save(current);
    }

I want to do an optimistic lock over this operation, so I have added a version field in the Note entity.

  1. How is version information affected by hibernate sessions? how does the version comparison actually happens?
  2. I tried simulation this scenario with some tests using multiple threads, but couldn't do so and couldn't verify if this works or not. is this method correct or wrong? Will the optimistic locking work if the object is in a detached state? or is there some rule which dictates when hibernate will check for the version matching between the old and new entity?
  3. In addition, do I need to explicitly annotate it with @Lock annotation? or it will pick up OPTIMISTIC_READ by default?

Solution

  • 2: No you don't need any @Lock for optimistic locking, using @Version we achieve optimistic locking.

    1: Now you can test optimistic locking scenario using Threading, below I have write some sample test code, you can update this based on your requirement.

    let suppose I have Note entity :

    @Entity
    @Data
    @ToString
    public class Note {
        @Id
        @GeneratedValue
        private Long id;
    
        @Version
        private int version;
    
        private String name;
    }
    

    Also I have Note Reposotory for performing DB oprration

    @Repository

    public interface NoteRepo extends JpaRepository<Note, Long> {
    }
    

    Now I have a Note Service

    @Service
    @AllArgsConstructor
    public class NoteService {
    
        private final NoteRepo noteRepo;
    
        @Transactional
        public Note create() {
            Note note = new Note();
            note.setName("Test Sample");
            note = noteRepo.save(note);
            System.out.println(note);
            return note;
        }
    
        @Transactional
        public void update(Long id, String name) {
           Note note =  noteRepo.getById(id);
           note.setName(name );
           note = noteRepo.save(note);
        }
    }
    

    Now time to Test Optimistic Locking with Integration Test.

    @ExtendWith(SpringExtension.class)
    @SpringBootTest(classes =Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class TestServiceTest {
     @Autowired
        private NoteService noteService;
    
        @Test
        void assertObjectOptimisticLockingFailure() throws InterruptedException {
            // Create a note, so after this we can perform updation
            Note note = noteService.create();
    
            //CountDownLatch required for wait test to complete
            CountDownLatch countDownLatch = new CountDownLatch(2);
    
           // Create 2 NoteThread to perform updation for exising note entity
            NoteThread xNote = new NoteThread("X", note.getId(), countDownLatch);
            NoteThread yNote = new NoteThread("Y", note.getId(), countDownLatch);
    
            xNote.start();
            yNote.start();
    
            countDownLatch.await();
    
            // Validate one of thread have ObjectOptimisticLockingFailureException
    
            assertTrue(xNote.hasObjectOptimisticLockingFailure() || yNote.hasObjectOptimisticLockingFailure());
    
        }
    
        class NoteThread extends Thread {
            private final String name;
            private final Long id;
            private final CountDownLatch countDownLatch;
            private Class exceptionClass;
            
            // verify exception
            boolean hasObjectOptimisticLockingFailure() {
                return this.exceptionClass == ObjectOptimisticLockingFailureException.class;
            }
    
           // Custome Thread class for performing note update and verify lock exception
            public NoteThread(String name, Long id, CountDownLatch countDownLatch) {
                this.name = name;
                this.id = id;
                this.countDownLatch = countDownLatch;
            }
    
            @Override
            public void run() {
                try {
                    noteService.update(id, name);
                } catch (Exception e) {
                    exceptionClass = e.getClass();
                }finally {
                    countDownLatch.countDown();
                }
            }
        }
    }