Search code examples
javaspring-boothibernateunit-testingoptimistic-locking

writing @Test for Hibernate Optimistic locking using @version is not working as expected


I have simple User and Account entities, they look like:

@Entity
@Table(name="USERS")
public class User {


    @NotNull
    @Column(name = "USERNAME",length = 50, unique = true)
    private String username;

    @NotNull
    @Column(name = "PASSWORD")
    private String password;

    @NotNull
    @Column(name = "ROLE",length = 20)
    @Enumerated(EnumType.STRING)
    private Role role;
}

And:

@Entity
@Table(name = "ACCOUNT")
public class Account {


    @NotNull
    @Column(name = "BALANCE")
    private BigDecimal balance;

    @JoinColumn(name = "USER_ID")
    @OneToOne(targetEntity = User.class, fetch = FetchType.LAZY)
    private User user;

    @Version
    private int version;

}

So I tried to write a @Test to become sure about it, and it is like:

    @Test
    public void test_optimistic_locking_concept() {
        User user = new User("test", "123456", Role.ROLE_USER);
        user = userRepository.save(user);

        Account account = new Account();
        account.setBalance(new BigDecimal("5000"));
        account.setUser(user);
        accountRepository.save(account);

        // fetching account record for different devices
        Account accountInDeviceOne = new Account();
        accountInDeviceOne = accountRepository.findAccountByUser_Username(user.getUsername()).get();

        Account accountInDeviceTwo = new Account();
        accountInDeviceTwo = accountRepository.findAccountByUser_Username(user.getUsername()).get();

        // each device tries to change the account balance by debit/credit
        accountInDeviceOne.setBalance(accountInDeviceOne.getBalance().subtract(new BigDecimal("1500")));
        accountInDeviceTwo.setBalance(accountInDeviceTwo.getBalance().add(new BigDecimal("2500")));

        // The versions of the updated accounts are both 0.
        Assertions.assertEquals(0, accountInDeviceOne.getVersion());
        Assertions.assertEquals(0, accountInDeviceTwo.getVersion());

        // first device request update
        accountInDeviceOne = accountRepository.save(accountInDeviceOne);

        // !Exception!
        accountInDeviceTwo = accountRepository.save(accountInDeviceTwo);
    }

But it doesn't throw Exception, as I expected!!

Also it does not increment the version field when I do accountRepository.save(accountInDeviceOne).

And in debugger console, as it is shown below, I don't know the reason why they are all pointing to the same resource!!!

enter image description here

Would someone please help me to understand what is going wrong here and how can I write test for this optimistic Locking Concept?

Any help would be appreciated!!


Solution

  • You can simply update a row in two concurrent threads like this:

    @SpringBootTest
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    public class SaveAccountOptimisticallyTest {
    
    @Autowired
    private AccountRepository repository;
    private Long savedAccountId;
    
    @BeforeEach
    public void insertUsers(){
        Account account = Account.builder()
                .balance(BigDecimal.ZERO)
                .build();
        savedAccountId = repository.save(account).getId();
    
    }
    
    @Test
    public void test_saving_account_optimistically() throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Account account = repository.findById(savedAccountId).orElse(null);
                try {
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setBalance(BigDecimal.ONE);
                try {
                    repository.save(account);
                }catch (Exception e){
                    Assertions.assertEquals(e.getCause(),"StaleObjectStateException");
                }
    
            }
        });
    
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Account account = repository.findById(savedAccountId).orElse(null);
                account.setBalance(BigDecimal.TEN);
                repository.save(account);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
    

    }

    Note that the first runnable will try to update the fetched object 6 seconds after retrieving it from db, and until then, the object's version field will be updated by other thread(t2) and this will cause an OptimisticLock (StaleObjectStateException of Hibernate) exception at t1.