56

How can I achieve the equivalent of this code:

tx.begin();
Widget w = em.find(Widget.class, 1L, LockModeType.PESSIMISTIC_WRITE);
w.decrementBy(4);
em.flush();
tx.commit();

... but using Spring and Spring-Data-JPA annotations?

The basis of my existing code is:

@Service
@Transactional(readOnly = true)
public class WidgetServiceImpl implements WidgetService
{
  /** The spring-data widget repository which extends CrudRepository<Widget, Long>. */
  @Autowired
  private WidgetRepository repo;

  @Transactional(readOnly = false)
  public void updateWidgetStock(Long id, int count)
  {
    Widget w = this.repo.findOne(id);
    w.decrementBy(4);
    this.repo.save(w);
  }
}

But I don't know how to specify that everything in the updateWidgetStock method should be done with a pessimistic lock set.

There is a Spring Data JPA annotation org.springframework.data.jpa.repository.Lock which allows you to set a LockModeType, but I don't know if it's valid to put it on the updateWidgetStock method. It sounds more like an annotation on the WidgetRepository, because the Javadoc says:

org.springframework.data.jpa.repository
@Target(value=METHOD)
@Retention(value=RUNTIME)
@Documented
public @interface Lock
Annotation used to specify the LockModeType to be used when executing the query. It will be evaluated when using Query on a query method or if you derive the query from the method name.

... so that doesn't seem to be helpful.

How can I make my updateWidgetStock() method execute with LockModeType.PESSIMISTIC_WRITE set?

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
Greg Kopff
  • 15,945
  • 12
  • 55
  • 78
  • Both answers to this question might be helpful: http://stackoverflow.com/questions/11880924/how-to-add-custom-method-to-spring-data-jpa – Greg Kopff Apr 23 '13 at 03:42

3 Answers3

80

@Lock is supported on CRUD methods as of version 1.6 of Spring Data JPA (in fact, there's already a milestone available). See this ticket for more details.

With that version you simply declare the following:

interface WidgetRepository extends Repository<Widget, Long> {

  @Lock(LockModeType.PESSIMISTIC_WRITE)
  Widget findOne(Long id);
}

This will cause the CRUD implementation part of the backing repository proxy to apply the configured LockModeType to the find(…) call on the EntityManager.

helvete
  • 2,455
  • 13
  • 33
  • 37
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • This @Lock already works with spring-data-jpa 1.4.1 in my testing... but anyway question is, how can i pass the lock type? I don't want to always use pessimistic lock, lots of senarios are mostly read. But when i know i'm doing writes, and that there will be contention i'd like to use a pessimistic lock. Can i create findOneLock(String, LockModeType)? – dan carter May 28 '14 at 04:09
  • 3
    ah ok, is see in 1.4.1 it's working when i add it to an overridden findOne, but not when i add it do my own findByXxx. The main question remains, how can i use a lock some of the time but not all of the time. – dan carter May 28 '14 at 04:51
  • 1
    Small question in regards with this. Will the annotation be taken into account if I annotate a method "findOnePessimstic(...)" with a default implementation calling findOne(...) ? This would allow to keep optimistic locking behavior for findOne, and offer pessimistic locking without the need of @Query. – Gauthier JACQUES Mar 16 '18 at 11:01
  • You should be aware that in the case you are trying to use spring-data-rest you are in trouble as spring data rest does read only transactions where doing "select for update" is illegal - and these call exactly findOne ... – Jan Zyka Oct 09 '18 at 20:03
  • At the moment @Lock is taken into account if it is placed on method name derived or query (not native). What if I need the same name derived method to be both pessimistic/no lock depending on the business logic? How to overcome it? – rilaby May 28 '20 at 21:02
23

If you don't want to override standard findOne() method, you can acquire a lock in your custom method by using select ... for update query just like this:

/**
 * Repository for Wallet.
 */
public interface WalletRepository extends CrudRepository<Wallet, Long>, JpaSpecificationExecutor<Wallet> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select w from Wallet w where w.id = :id")
    Wallet findOneForUpdate(@Param("id") Long id);
}

However, if you are using PostgreSQL, things can get a little complicated when you want to set lock timeout to avoid deadlocks. PostgreSQL ignores standard property javax.persistence.lock.timeout set in JPA properties or in @QueryHint annotation.

The only way I could get it working was to create a custom repository and set timeout manually before locking an entity. It's not nice but at least it's working:

public class WalletRepositoryImpl implements WalletRepositoryCustom {

@PersistenceContext
private EntityManager em;


@Override
public Wallet findOneForUpdate(Long id) {
    // explicitly set lock timeout (necessary in PostgreSQL)
    em.createNativeQuery("set local lock_timeout to '2s';").executeUpdate();

    Wallet wallet = em.find(Wallet.class, id);

    if (wallet != null) {
        em.lock(wallet, LockModeType.PESSIMISTIC_WRITE);
    }

    return wallet;
}

}

petr.vlcek
  • 285
  • 2
  • 6
  • 3
    Unfortunately, this does not refresh the `entityManager` cache. You must manually do an `entityManager.refresh(wallet)` – rcomblen Nov 11 '16 at 09:38
  • why would you want to have a timeout if lock ends at the end of transaction anyway? – Benas Feb 13 '23 at 17:12
  • could you please provide some supporting evidence for: PostgreSQL ignores standard property javax.persistence.lock.timeout set in JPA properties or in @QueryHint annotation. I am using: ``` @Lock(PESSIMISTIC_WRITE) @QueryHints(QueryHint(name = JPA_LOCK_TIMEOUT, value = SKIP_LOCKED)) ``` on a method and it translates correctly into query having `... skip locked` suffix. – Michal Hosala Feb 21 '23 at 16:49
14

If you are able to use Spring Data 1.6 or greater than ignore this answer and refer to Oliver's answer.

The Spring Data pessimistic @Lock annotations only apply (as you pointed out) to queries. There are not annotations I know of which can affect an entire transaction. You can either create a findByOnePessimistic method which calls findByOne with a pessimistic lock or you can change findByOne to always obtain a pessimistic lock.

If you wanted to implement your own solution you probably could. Under the hood the @Lock annotation is processed by LockModePopulatingMethodIntercceptor which does the following:

TransactionSynchronizationManager.bindResource(method, lockMode == null ? NULL : lockMode);

You could create some static lock manager which had a ThreadLocal<LockMode> member variable and then have an aspect wrapped around every method in every repository which called bindResource with the lock mode set in the ThreadLocal. This would allow you to set the lock mode on a per-thread basis. You could then create your own @MethodLockMode annotation which would wrap the method in an aspect which sets the thread-specific lock mode before running the method and clears it after running the method.

Pace
  • 41,875
  • 13
  • 113
  • 156
  • What's the point of having a pessimistic lock on a query if it doesn't hold until the end of the transaction (and thus encompass any following updates)? – Greg Kopff Apr 26 '13 at 00:17
  • A pessimistic lock obtained in a query should hold until the end of the transaction. Those second two paragraphs were describing how you could create a transaction where all queries obtained pessimistic locks. – Pace Apr 26 '13 at 13:32
  • 2
    If I understand correctly then, I can achieve the effect I want simply by making `findOne` use a pessimistic lock - that lock should hold until the end of my transaction (which will end at the end of my `updateWidgetStock()` method. Is that right? – Greg Kopff Apr 27 '13 at 06:57
  • 2
    Note, that using `@Lock` will be supported on CRUD methods as of the upcoming 1.6 release of Spring Data JPA. See this [ticket](https://jira.spring.io/browse/DATAJPA-173) for details. – Oliver Drotbohm Apr 23 '14 at 06:10