vratojr
vratojr

Reputation: 845

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> {
}

Upvotes: 14

Views: 22416

Answers (5)

Mike
Mike

Reputation: 421

to answer you question, no. But, if you want to work with a your entity and need the new Version after save(), then yes, or use flush(). Reason is the optimistic locking is determined at the commit (or flush) of the transaction.

Upvotes: 0

Jens Schauder
Jens Schauder

Reputation: 81988

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.

Upvotes: 14

Har Krishan
Har Krishan

Reputation: 273

You can set the optimistic-lock strategy like:

optimistic-lock (optional - defaults to version): determines the optimistic locking strategy.

optimistic locking strategies: version: check the version/timestamp columns, all: check all columns, dirty: check the changed columns none: do not use optimistic locking

optimistic locking is completely handled by Hibernate.

Optimistic locking Concept

Scenario to use: when concurrent updates are rare at the end of transaction check if updated by any other transaction

Concurrent updates can be handled using optimistic locking. Optimistic locking works by checking whether the data it is about to update has been changed by another transaction since it was read. e.g. you searched a record, after long time you are going to modify that record but in the meantime record has been updated by someone else. One common way to implement optimistic locking is to add a version column to each table, which is incremented by the application each time it changes a row. Each UPDATE statement's WHERE clause checks that the version number has not changed since it was read. If row has been updated or deleted by another transaction, application can roll back the transaction and start over. Optimistic locking derives its name from the fact it assumes that concurrent updates are rare, instead of preventing them, application detects and recovers from them. The Optimistic Lock pattern only detects changes when the user tries to save changes, it only works well when starting over is not a burden on user. When implementing use-cases where user would be extremely annoyed by having to discard several minutes' work, a much better option is to use the Pessimistic Lock.

Upvotes: -1

Amr Alaa
Amr Alaa

Reputation: 553

The @Version annotation is used as a column in the database that should be added to each entity to perform the optimistic locking for this entity for example

@Entity
public class User {
    @Version
    @Column(nullable = false)
    private Long version;
}

This will make sure that no user will be created with wrong version. Which means that you cannot update the user from multiple sources at the same time.

Upvotes: -2

Veselin Davidov
Veselin Davidov

Reputation: 7081

The reason behind optimistic lock is to prevent updates on a table from previous state. For example:

  1. You get user with id 1
  2. Another user updates and commit user with id 1 to a new state
  3. You update user 1 (which you loaded in step 1) and try to commit it to the database

In this case in step 3 you will override the changes the other guy did in step 2 and instead you need to throw an exception.

I believe spring does it with the @version property which corresponds to a version column in the database. The result is something like:

update users set password="p2" where id=1 and version=1; 

I think spring actually uses string for version but I am not sure. Probably a timestamp too but that's the general idea.

You don't get an exception because only one of your threads is manipulating the data. You read it in thread 1 and current version is for example 1, then in thread 2 you read it - version is still 1. Then when you try to save it compares the version in the hibernate session to the one in the database and they match - everything is in order so it continues without exception. Try to make it updateWithSleep() and you should get the expected exception.

Upvotes: -1

Related Questions