Issue
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 href="http://download.oracle.com/otn-pub/jcp/persistence-2_1-pr-spec/JavaPersistencePDR.pdf" rel="noreferrer">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> {
}
Solution
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.
Answered By - Jens Schauder
Answer Checked By - David Marino (JavaFixing Volunteer)