Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring data - enable optimistic locking

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> {
}
like image 204
vratojr Avatar asked Nov 14 '17 09:11

vratojr


People also ask

Is optimistic locking necessary?

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.

How do I fix optimistic lock?

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.

How do you implement optimistic locking in JPA?

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.

Is optimistic locking enabled by default?

The Enable Optimistic Locking option is enabled by default for all custom record types created as of 2012.2 and later.


1 Answers

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 to EntityManager.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.

like image 141
Jens Schauder Avatar answered Oct 03 '22 05:10

Jens Schauder