10. Locking

In a relational database, locking refers to actions taken to prevent data from changing between the time it is read and the time is used.

Your locking strategy can be either optimistic or pessimistic.

Optimistic

Optimistic locking assumes that multiple transactions can complete without affecting each other, and that therefore transactions can proceed without locking the data resources that they affect. Before committing, each transaction verifies that no other transaction has modified its data. If the check reveals conflicting modifications, the committing transaction rolls back.

Pessimistic

Pessimistic locking assumes that concurrent transactions will conflict with each other, and requires resources to be locked after they are read and only unlocked after the application has finished using the data.

Hibernate provides mechanisms for implementing both types of locking in your applications.

10.1. Optimistic

When your application uses long transactions or conversations that span several database transactions, you can store versioning data so that if the same entity is updated by two conversations, the last to commit changes is informed of the conflict, and does not override the other conversation’s work. This approach guarantees some isolation, but scales well and works particularly well in read-often-write-sometimes situations.

Hibernate provides two different mechanisms for storing versioning information, a dedicated version number or a timestamp.

A version or timestamp property can never be null for a detached instance. Hibernate detects any instance with a null version or timestamp as transient, regardless of other unsaved-value strategies that you specify. Declaring a nullable version or timestamp property is an easy way to avoid problems with transitive reattachment in Hibernate, especially useful if you use assigned identifiers or composite keys.

10.1.1. Mapping optimistic locking

JPA defines support for optimistic locking based on either a version (sequential numeric) or timestamp strategy. To enable this style of optimistic locking simply add the javax.persistence.Version to the persistent attribute that defines the optimistic locking value. According to JPA, the valid types for these attributes are limited to:

  • int or Integer

  • short or Short

  • long or Long

  • java.sql.Timestamp

However, Hibernate allows you to use even Java 8 Date/Time types, such as Instant.

Example 387. @Version annotation mapping

  1. @Entity(name = "Person")
  2. public static class Person {
  3. @Id
  4. @GeneratedValue
  5. private Long id;
  6. @Column(name = "`name`")
  7. private String name;
  8. @Version
  9. private long version;
  10. //Getters and setters are omitted for brevity
  11. }
  1. @Entity(name = "Person")
  2. public static class Person {
  3. @Id
  4. @GeneratedValue
  5. private Long id;
  6. @Column(name = "`name`")
  7. private String name;
  8. @Version
  9. private Timestamp version;
  10. //Getters and setters are omitted for brevity
  11. }
  1. @Entity(name = "Person")
  2. public static class Person {
  3. @Id
  4. @GeneratedValue
  5. private Long id;
  6. @Column(name = "`name`")
  7. private String name;
  8. @Version
  9. private Instant version;
  10. //Getters and setters are omitted for brevity
  11. }
Dedicated version number

The version number mechanism for optimistic locking is provided through a @Version annotation.

Example 388. @Version annotation

  1. @Version
  2. private long version;

Here, the version property is mapped to the version column, and the entity manager uses it to detect conflicting updates, and prevent the loss of updates that would otherwise be overwritten by a last-commit-wins strategy.

The version column can be any kind of type, as long as you define and implement the appropriate UserVersionType.

Your application is forbidden from altering the version number set by Hibernate. To artificially increase the version number, see the documentation for properties LockModeType.OPTIMISTIC_FORCE_INCREMENT or LockModeType.PESSIMISTIC_FORCE_INCREMENT check in the Hibernate Entity Manager reference documentation.

If the version number is generated by the database, such as a trigger, use the annotation @org.hibernate.annotations.Generated(GenerationTime.ALWAYS) on the version attribute.

Timestamp

Timestamps are a less reliable way of optimistic locking than version numbers but can be used by applications for other purposes as well. Timestamping is automatically used if you the @Version annotation on a Date or Calendar property type.

Example 389. Using timestamps for optimistic locking

  1. @Version
  2. private Date version;

Hibernate can retrieve the timestamp value from the database or the JVM, by reading the value you specify for the @org.hibernate.annotations.Source annotation. The value can be either org.hibernate.annotations.SourceType.DB or org.hibernate.annotations.SourceType.VM. The default behavior is to use the database, and database is also used if you don’t specify the annotation at all.

The timestamp can also be generated by the database instead of Hibernate if you use the @org.hibernate.annotations.Generated(GenerationTime.ALWAYS) or the @Source annotation.

Example 390. Database-generated version timestamp mapping

  1. @Entity(name = "Person")
  2. public static class Person {
  3. @Id
  4. private Long id;
  5. private String firstName;
  6. private String lastName;
  7. @Version
  8. @Source(value = SourceType.DB)
  9. private Date version;
  10. }

Now, when persisting a Person entity, Hibernate calls the database-specific current timestamp retrieval function:

Example 391. Database-generated version timestamp example

  1. Person person = new Person();
  2. person.setId( 1L );
  3. person.setFirstName( "John" );
  4. person.setLastName( "Doe" );
  5. assertNull( person.getVersion() );
  6. entityManager.persist( person );
  7. assertNotNull( person.getVersion() );
  1. CALL current_timestamp()
  2. INSERT INTO
  3. Person
  4. (firstName, lastName, version, id)
  5. VALUES
  6. (?, ?, ?, ?)
  7. -- binding parameter [1] as [VARCHAR] - [John]
  8. -- binding parameter [2] as [VARCHAR] - [Doe]
  9. -- binding parameter [3] as [TIMESTAMP] - [2017-05-18 12:03:03.808]
  10. -- binding parameter [4] as [BIGINT] - [1]
Excluding attributes

By default, every entity attribute modification is going to trigger a version incrementation. If there is an entity property which should not bump up the entity version, then you need to annotate it with the Hibernate @OptimisticLock annotation, as illustrated in the following example.

Example 392. @OptimisticLock mapping example

  1. @Entity(name = "Phone")
  2. public static class Phone {
  3. @Id
  4. private Long id;
  5. @Column(name = "`number`")
  6. private String number;
  7. @OptimisticLock( excluded = true )
  8. private long callCount;
  9. @Version
  10. private Long version;
  11. //Getters and setters are omitted for brevity
  12. public void incrementCallCount() {
  13. this.callCount++;
  14. }
  15. }

This way, if one thread modifies the Phone number while a second thread increments the callCount attribute, the two concurrent transactions are not going to conflict as illustrated by the following example.

Example 393. @OptimisticLock exlude attribute example

  1. doInJPA( this::entityManagerFactory, entityManager -> {
  2. Phone phone = entityManager.find( Phone.class, 1L );
  3. phone.setNumber( "+123-456-7890" );
  4. doInJPA( this::entityManagerFactory, _entityManager -> {
  5. Phone _phone = _entityManager.find( Phone.class, 1L );
  6. _phone.incrementCallCount();
  7. log.info( "Bob changes the Phone call count" );
  8. } );
  9. log.info( "Alice changes the Phone number" );
  10. } );
  1. -- Bob changes the Phone call count
  2. update
  3. Phone
  4. set
  5. callCount = 1,
  6. "number" = '123-456-7890',
  7. version = 0
  8. where
  9. id = 1
  10. and version = 0
  11. -- Alice changes the Phone number
  12. update
  13. Phone
  14. set
  15. callCount = 0,
  16. "number" = '+123-456-7890',
  17. version = 1
  18. where
  19. id = 1
  20. and version = 0

When Bob changes the Phone entity callCount, the entity version is not bumped up. That’s why Alice’s UPDATE succeeds since the entity version is still 0, even if Bob has changed the record since Alice loaded it.

Although there is no conflict between Bob and Alice, Alice’s UPDATE overrides Bob’s change to the callCount attribute.

For this reason, you should only use this feature if you can accommodate lost updates on the excluded entity properties.

Versionless optimistic locking

Although the default @Version property optimistic locking mechanism is sufficient in many situations, sometimes, you need rely on the actual database row column values to prevent lost updates.

Hibernate supports a form of optimistic locking that does not require a dedicated “version attribute”. This is also useful for use with modeling legacy schemas.

The idea is that you can get Hibernate to perform “version checks” using either all of the entity’s attributes or just the attributes that have changed. This is achieved through the use of the @OptimisticLocking annotation which defines a single attribute of type org.hibernate.annotations.OptimisticLockType.

There are 4 available OptimisticLockTypes:

NONE

optimistic locking is disabled even if there is a @Version annotation present

VERSION (the default)

performs optimistic locking based on a @Version as described above

ALL

performs optimistic locking based on all fields as part of an expanded WHERE clause restriction for the UPDATE/DELETE SQL statements

DIRTY

performs optimistic locking based on dirty fields as part of an expanded WHERE clause restriction for the UPDATE/DELETE SQL statements

Versionless optimistic locking using OptimisticLockType.ALL

Example 394. OptimisticLockType.ALL mapping example

  1. @Entity(name = "Person")
  2. @OptimisticLocking(type = OptimisticLockType.ALL)
  3. @DynamicUpdate
  4. public static class Person {
  5. @Id
  6. private Long id;
  7. @Column(name = "`name`")
  8. private String name;
  9. private String country;
  10. private String city;
  11. @Column(name = "created_on")
  12. private Timestamp createdOn;
  13. //Getters and setters are omitted for brevity
  14. }

When you need to modify the Person entity above:

Example 395. OptimisticLockType.ALL update example

  1. Person person = entityManager.find( Person.class, 1L );
  2. person.setCity( "Washington D.C." );
  1. UPDATE
  2. Person
  3. SET
  4. city=?
  5. WHERE
  6. id=?
  7. AND city=?
  8. AND country=?
  9. AND created_on=?
  10. AND "name"=?
  11. -- binding parameter [1] as [VARCHAR] - [Washington D.C.]
  12. -- binding parameter [2] as [BIGINT] - [1]
  13. -- binding parameter [3] as [VARCHAR] - [New York]
  14. -- binding parameter [4] as [VARCHAR] - [US]
  15. -- binding parameter [5] as [TIMESTAMP] - [2016-11-16 16:05:12.876]
  16. -- binding parameter [6] as [VARCHAR] - [John Doe]

As you can see, all the columns of the associated database row are used in the WHERE clause. If any column has changed after the row was loaded, there won’t be any match, and a StaleStateException or an OptimisticLockException is going to be thrown.

When using OptimisticLockType.ALL, you should also use @DynamicUpdate because the UPDATE statement must take into consideration all the entity property values.

Versionless optimistic locking using OptimisticLockType.DIRTY

The OptimisticLockType.DIRTY differs from OptimisticLockType.ALL in that it only takes into consideration the entity properties that have changed since the entity was loaded in the currently running Persistence Context.

Example 396. OptimisticLockType.DIRTY mapping example

  1. @Entity(name = "Person")
  2. @OptimisticLocking(type = OptimisticLockType.DIRTY)
  3. @DynamicUpdate
  4. @SelectBeforeUpdate
  5. public static class Person {
  6. @Id
  7. private Long id;
  8. @Column(name = "`name`")
  9. private String name;
  10. private String country;
  11. private String city;
  12. @Column(name = "created_on")
  13. private Timestamp createdOn;
  14. //Getters and setters are omitted for brevity
  15. }

When you need to modify the Person entity above:

Example 397. OptimisticLockType.DIRTY update example

  1. Person person = entityManager.find( Person.class, 1L );
  2. person.setCity( "Washington D.C." );
  1. UPDATE
  2. Person
  3. SET
  4. city=?
  5. WHERE
  6. id=?
  7. and city=?
  8. -- binding parameter [1] as [VARCHAR] - [Washington D.C.]
  9. -- binding parameter [2] as [BIGINT] - [1]
  10. -- binding parameter [3] as [VARCHAR] - [New York]

This time, only the database column that has changed was used in the WHERE clause.

The main advantage of OptimisticLockType.DIRTY over OptimisticLockType.ALL and the default OptimisticLockType.VERSION used implicitly along with the @Version mapping, is that it allows you to minimize the risk of OptimisticLockException across non-overlapping entity property changes.

When using OptimisticLockType.DIRTY, you should also use @DynamicUpdate because the UPDATE statement must take into consideration all the dirty entity property values, and also the @SelectBeforeUpdate annotation so that detached entities are properly handled by the Session#update(entity) operation.

10.2. Pessimistic

Typically, you only need to specify an isolation level for the JDBC connections and let the database handle locking issues. If you do need to obtain exclusive pessimistic locks or re-obtain locks at the start of a new transaction, Hibernate gives you the tools you need.

Hibernate always uses the locking mechanism of the database, and never lock objects in memory.

10.3. LockMode and LockModeType

Long before JPA 1.0, Hibernate already defined various explicit locking strategies through its LockMode enumeration. JPA comes with its own LockModeType enumeration which defines similar strategies as the Hibernate-native LockMode.

LockModeTypeLockModeDescription

NONE

NONE

The absence of a lock. All objects switch to this lock mode at the end of a Transaction. Objects associated with the session via a call to update() or saveOrUpdate() also start out in this lock mode.

READ and OPTIMISTIC

READ

The entity version is checked towards the end of the currently running transaction.

WRITE and OPTIMISTIC_FORCE_INCREMENT

WRITE

The entity version is incremented automatically even if the entity has not changed.

PESSIMISTIC_FORCE_INCREMENT

PESSIMISTIC_FORCE_INCREMENT

The entity is locked pessimistically and its version is incremented automatically even if the entity has not changed.

PESSIMISTIC_READ

PESSIMISTIC_READ

The entity is locked pessimistically using a shared lock if the database supports such a feature. Otherwise, an explicit lock is used.

PESSIMISTIC_WRITE

PESSIMISTIC_WRITE, UPGRADE

The entity is locked using an explicit lock.

PESSIMISTIC_WRITE with a javax.persistence.lock.timeout setting of 0

UPGRADE_NOWAIT

The lock acquisition request fails fast if the row s already locked.

PESSIMISTIC_WRITE with a javax.persistence.lock.timeout setting of -2

UPGRADE_SKIPLOCKED

The lock acquisition request skips the already locked rows. It uses a SELECT …​ FOR UPDATE SKIP LOCKED in Oracle and PostgreSQL 9.5, or SELECT …​ with (rowlock, updlock, readpast) in SQL Server.

The explicit user request mentioned above occurs as a consequence of any of the following actions:

  • a call to Session.load(), specifying a LockMode.

  • a call to Session.lock().

  • a call to Query.setLockMode().

If you call Session.load() with option UPGRADE, UPGRADE_NOWAIT or UPGRADE_SKIPLOCKED, and the requested object is not already loaded by the session, the object is loaded using SELECT …​ FOR UPDATE.

If you call load() for an object that is already loaded with a less restrictive lock than the one you request, Hibernate calls lock() for that object.

Session.lock() performs a version number check if the specified lock mode is READ, UPGRADE, UPGRADE_NOWAIT or UPGRADE_SKIPLOCKED. In the case of UPGRADE, UPGRADE_NOWAIT or UPGRADE_SKIPLOCKED, the SELECT …​ FOR UPDATE syntax is used.

If the requested lock mode is not supported by the database, Hibernate uses an appropriate alternate mode instead of throwing an exception. This ensures that applications are portable.

10.4. JPA locking query hints

JPA 2.0 introduced two query hints:

javax.persistence.lock.timeout

it gives the number of milliseconds a lock acquisition request will wait before throwing an exception

javax.persistence.lock.scope

defines the scope of the lock acquisition request. The scope can either be NORMAL (default value) or EXTENDED. The EXTENDED scope will cause a lock acquisition request to be passed to other owned table structured (e.g. @Inheritance(strategy=InheritanceType.JOINED), @ElementCollection)

Example 398. javax.persistence.lock.timeout example

  1. entityManager.find(
  2. Person.class, id, LockModeType.PESSIMISTIC_WRITE,
  3. Collections.singletonMap( "javax.persistence.lock.timeout", 200 )
  4. );
  1. SELECT explicitlo0_.id AS id1_0_0_,
  2. explicitlo0_."name" AS name2_0_0_
  3. FROM person explicitlo0_
  4. WHERE explicitlo0_.id = 1
  5. FOR UPDATE wait 2

Not all JDBC database drivers support setting a timeout value for a locking request. If not supported, the Hibernate dialect ignores this query hint.

The javax.persistence.lock.scope is not yet supported as specified by the JPA standard.

10.5. The buildLockRequest API

Traditionally, Hibernate offered the Session#lock() method for acquiring an optimistic or a pessimistic lock on a given entity. Because varying the locking options was difficult when using a single LockMode parameter, Hibernate has added the Session#buildLockRequest() method API.

The following example shows how to obtain a shared database lock without waiting for the lock acquisition request.

Example 399. buildLockRequest example

  1. Person person = entityManager.find( Person.class, id );
  2. Session session = entityManager.unwrap( Session.class );
  3. session
  4. .buildLockRequest( LockOptions.NONE )
  5. .setLockMode( LockMode.PESSIMISTIC_READ )
  6. .setTimeOut( LockOptions.NO_WAIT )
  7. .lock( person );
  1. SELECT p.id AS id1_0_0_ ,
  2. p.name AS name2_0_0_
  3. FROM Person p
  4. WHERE p.id = 1
  5. SELECT id
  6. FROM Person
  7. WHERE id = 1
  8. FOR SHARE NOWAIT

10.6. Follow-on-locking

When using Oracle, the FOR UPDATE exclusive locking clause cannot be used with:

  • DISTINCT

  • GROUP BY

  • UNION

  • inlined views (derived tables), therefore, affecting the legacy Oracle pagination mechanism as well.

For this reason, Hibernate uses secondary selects to lock the previously fetched entities.

Example 400. Follow-on-locking example

  1. List<Person> persons = entityManager.createQuery(
  2. "select DISTINCT p from Person p", Person.class)
  3. .setLockMode( LockModeType.PESSIMISTIC_WRITE )
  4. .getResultList();
  1. SELECT DISTINCT p.id as id1_0_, p."name" as name2_0_
  2. FROM Person p
  3. SELECT id
  4. FROM Person
  5. WHERE id = 1 FOR UPDATE
  6. SELECT id
  7. FROM Person
  8. WHERE id = 1 FOR UPDATE

To avoid the N+1 query problem, a separate query can be used to apply the lock using the associated entity identifiers.

Example 401. Secondary query entity locking

  1. List<Person> persons = entityManager.createQuery(
  2. "select DISTINCT p from Person p", Person.class)
  3. .getResultList();
  4. entityManager.createQuery(
  5. "select p.id from Person p where p in :persons")
  6. .setLockMode( LockModeType.PESSIMISTIC_WRITE )
  7. .setParameter( "persons", persons )
  8. .getResultList();
  1. SELECT DISTINCT p.id as id1_0_, p."name" as name2_0_
  2. FROM Person p
  3. SELECT p.id as col_0_0_
  4. FROM Person p
  5. WHERE p.id IN ( 1 , 2 )
  6. FOR UPDATE

The lock request was moved from the original query to a secondary one which takes the previously fetched entities to lock their associated database records.

Prior to Hibernate 5.2.1, the follow-on-locking mechanism was applied uniformly to any locking query executing on Oracle. Since 5.2.1, the Oracle Dialect tries to figure out if the current query demands the follow-on-locking mechanism.

Even more important is that you can overrule the default follow-on-locking detection logic and explicitly enable or disable it on a per query basis.

Example 402. Disabling the follow-on-locking mechanism explicitly

  1. List<Person> persons = entityManager.createQuery(
  2. "select p from Person p", Person.class)
  3. .setMaxResults( 10 )
  4. .unwrap( Query.class )
  5. .setLockOptions(
  6. new LockOptions( LockMode.PESSIMISTIC_WRITE )
  7. .setFollowOnLocking( false ) )
  8. .getResultList();
  1. SELECT *
  2. FROM (
  3. SELECT p.id as id1_0_, p."name" as name2_0_
  4. FROM Person p
  5. )
  6. WHERE rownum <= 10
  7. FOR UPDATE

The follow-on-locking mechanism should be explicitly enabled only if the currently executing query fails because the FOR UPDATE clause cannot be applied, meaning that the Dialect resolving mechanism needs to be further improved.