Note: I DON'T NEED AN EXPLANATION CONCERNING THE OPTIMISTIC LOCKING. This question is about what seems to be a specific Spring Data behavior when using optimistic locking.
From the jpa specs whenever an entity has a @Version
annotated field, optimistic locking should be enabled automatically on the entity.
If I do this in a spring data test project using Repositories, the locking seems to not be activated. Infact no OptimisticLockException
is thrown while doing a Non Repetable Read test (see P2 on page 93 of the JPA specs)
However, from spring docs I see that if we annotate a single method with @Lock(LockModeType.OPTIMISTIC)
then the underlying system correctly throws an OptimisticLockException
(that is then catch by spring and propagated up the stack in a slightly different form).
Is this normal or did I miss something? Are we obliged to annotate all our methods (or to create a base repository implementation that takes the lock) to have optimistic behavior enabled with spring data?
I'm using spring data in the context of a spring boot project, version 1.4.5.
The test:
public class OptimisticLockExceptionTest {
static class ReadWithSleepRunnable extends Thread {
private OptimisticLockExceptionService service;
private int id;
UserRepository userRepository;
public ReadWithSleepRunnable(OptimisticLockExceptionService service, int id, UserRepository userRepository) {
this.service = service;
this.id = id;
this.userRepository = userRepository;
}
@Override
public void run() {
this.service.readWithSleep(this.userRepository, this.id);
}
}
static class ModifyRunnable extends Thread {
private OptimisticLockExceptionService service;
private int id;
UserRepository userRepository;
public ModifyRunnable(OptimisticLockExceptionService service, int id, UserRepository userRepository) {
this.service = service;
this.id = id;
this.userRepository = userRepository;
}
@Override
public void run() {
this.service.modifyUser(this.userRepository, this.id);
}
}
@Inject
private OptimisticLockExceptionService service;
@Inject
private UserRepository userRepository;
private User u;
@Test(expected = ObjectOptimisticLockingFailureException.class)
public void thatOptimisticLockExceptionIsThrown() throws Exception {
this.u = new User("email", "p");
this.u = this.userRepository.save(this.u);
try {
Thread t1 = new ReadWithSleepRunnable(this.service, this.u.getId(), this.userRepository);
t1.start();
Thread.sleep(50);// To be sure the submitted thread starts
assertTrue(t1.isAlive());
Thread t2 = new ModifyRunnable(this.service, this.u.getId(), this.userRepository);
t2.start();
t2.join();
assertTrue(t1.isAlive());
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
The test service:
@Component
public class OptimisticLockExceptionService {
@Transactional
public User readWithSleep(UserRepository userRepo, int id) {
System.err.println("started read");
User op = userRepo.findOne(id);
Thread.currentThread();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("read end");
return op;
}
@Transactional
public User modifyUser(UserRepository userRepo, int id) {
System.err.println("started modify");
User op = userRepo.findOne(id);
op.setPassword("p2");
System.err.println("modify end");
return userRepo.save(op);
}
}
The repository:
@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
}
Optimistic locking might not be the best solution for every situation. While some use-cases work well with optimistic locking, others might need stricter schemes like pessimistic locking. Locking might not be good for all cases – your application can have a problem if there is a lock contention.
Solution. To resolve this error we have two ways: Get the latest object from the database and set the old object values if you need those values to be persisted to the new object and merge it. For the old object set the latest version from Database.
In order to use optimistic locking, we need to have an entity including a property with @Version annotation. While using it, each transaction that reads data holds the value of the version property. Before the transaction wants to make an update, it checks the version property again.
The Enable Optimistic Locking option is enabled by default for all custom record types created as of 2012.2 and later.
Optimistic Locking with Spring Data JPA is implemented by the JPA implementation used.
You are referring to P2 on page 93 of the JPA specs. The section starts with:
If transaction T1 calls
lock(entity, LockModeType.OPTIMISTIC)
on a versioned object, the entity manager must ensure that neither of the following phenomena can occur:
But your test doesn't create such a scenario. The method lock
never gets called. Therefore no relevant locking happens. Especially just loading an entity doesn't call lock
on it.
Things change when one modifies an object (Page 93 second but last paragraph of the spec):
If a versioned object is otherwise updated or removed, then the implementation must ensure that the requirements of
LockModeType.OPTIMISTIC_FORCE_INCREMENT
are met, even if no explicit call toEntityManager.lock
was made.
Note: you are spawning two threads using the same repository, which in turn will make them use the same EntityManager
. I doubt if this is supported by EntityManager
and also I'm not sure if you are actually getting two transactions at all this way, but that is a question for another day.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With