Bloqueo optimista en JPA

1. Introducción

Cuando se trata de aplicaciones empresariales, es crucial administrar el acceso concurrente a una base de datos de manera adecuada. Esto significa que deberíamos poder manejar múltiples transacciones de una manera efectiva y, lo más importante, a prueba de errores.

Además, debemos asegurarnos de que los datos se mantengan coherentes entre las lecturas y actualizaciones simultáneas.

Para lograrlo, podemos utilizar el mecanismo de bloqueo optimista proporcionado por la API de persistencia de Java. Conduce a que múltiples actualizaciones realizadas en los mismos datos al mismo tiempo no interfieran entre sí.

2. Comprensión del bloqueo optimista

Para utilizar el bloqueo optimista, necesitamos tener una entidad que incluya una propiedad con la anotación @Version . Mientras lo usa, cada transacción que lee datos tiene el valor de la propiedad de la versión.

Antes de que la transacción desee realizar una actualización, vuelve a comprobar la propiedad de la versión.

Si el valor ha cambiado mientras tanto, se lanza una OptimisticLockException . De lo contrario, la transacción confirma la actualización e incrementa una propiedad de versión de valor.

3. Bloqueo pesimista frente a bloqueo optimista

Es bueno saber que, en contraste con el bloqueo optimista, JPA nos da un bloqueo pesimista. Es otro mecanismo para manejar el acceso concurrente de datos.

Cubrimos el bloqueo pesimista en uno de nuestros artículos anteriores: Bloqueo pesimista en JPA. Averigüemos cuál es la diferencia y cómo podemos beneficiarnos de cada tipo de bloqueo.

Como dijimos antes, el bloqueo optimista se basa en detectar cambios en las entidades comprobando su atributo de versión . Si se lleva a cabo alguna actualización simultánea, se produce OptmisticLockException . Después de eso, podemos volver a intentar actualizar los datos.

Podemos imaginar que este mecanismo es adecuado para aplicaciones que hacen muchas más lecturas que actualizaciones o eliminaciones. Además, es útil en situaciones en las que las entidades deben estar desconectadas durante algún tiempo y no se pueden mantener bloqueos.

Por el contrario, el mecanismo de bloqueo pesimista implica el bloqueo de entidades en el nivel de la base de datos.

Cada transacción puede adquirir un bloqueo de datos. Mientras mantenga el bloqueo, ninguna transacción puede leer, eliminar o realizar actualizaciones en los datos bloqueados. Podemos suponer que el uso de bloqueo pesimista puede resultar en interbloqueos. Sin embargo, garantiza una mayor integridad de los datos que el bloqueo optimista.

4. Atributos de la versión

Los atributos de versión son propiedades con anotación @Version . Son necesarios para permitir un bloqueo optimista. Veamos una clase de entidad de muestra:

@Entity public class Student { @Id private Long id; private String name; private String lastName; @Version private Integer version; // getters and setters }

Hay varias reglas que debemos seguir al declarar los atributos de la versión:

  • cada clase de entidad debe tener solo un atributo de versión
  • debe colocarse en la tabla principal para una entidad asignada a varias tablas
  • el tipo de un atributo de versión debe ser uno de los siguientes: int , Integer , long , Long , short , Short , java.sql.Timestamp

Debemos saber que podemos recuperar un valor del atributo de versión a través de la entidad, pero no debemos actualizarlo ni incrementarlo. Solo el proveedor de persistencia puede hacer eso, por lo que los datos se mantienen consistentes.

Vale la pena notar que los proveedores de persistencia pueden admitir el bloqueo optimista para entidades que no tienen atributos de versión. Sin embargo, es una buena idea incluir siempre los atributos de la versión cuando se trabaja con bloqueo optimista.

Si intentamos bloquear una entidad que no contiene tal atributo y el proveedor de persistencia no lo admite, resultará en una PersitenceException .

5. Modos de bloqueo

JPA nos proporciona dos modos de bloqueo optimistas diferentes (y dos alias):

  • OPTIMISTIC : obtiene un bloqueo de lectura optimista para todas las entidades que contienen un atributo de versión
  • OPTIMISTIC_FORCE_INCREMENT : obtiene un bloqueo optimista al igual que OPTIMISTIC y además incrementa el valor del atributo de versión
  • LEER - es sinónimo de OPTIMISTIC
  • WRITE : es un sinónimo de OPTIMISTIC_FORCE_INCREMENT

Podemos encontrar todos los tipos enumerados anteriormente en la clase LockModeType .

5.1. OPTIMISTIC (READ)

As we already know, OPTIMISTIC and READ lock modes are synonyms. However, JPA specification recommends us to use OPTIMISTIC in new applications.

Whenever we request the OPTIMISTIC lock mode, a persistence provider will prevent our data from dirty reads as well as non-repeatable reads.

Put simply, it should ensure any transaction fails to commit any modification on data that another transaction:

  • has updated or deleted but not committed
  • has updated or deleted successfully in the meantime

5.2. OPTIMISTIC_INCREMENT (WRITE)

The same as previously, OPTIMISTIC_INCREMENT and WRITE are synonyms, but the former is preferable.

OPTIMISTIC_INCREMENT must meet the same conditions as OPTIMISTIC lock mode. Additionally, it increments the value of a version attribute. However, it's not specified whether it should be done immediately or may be put off until commit or flush.

It's worth to know that a persistence provider is allowed to provide OPTIMISTIC_INCREMENT functionality when OPTIMISTIC lock mode is requested.

6. Using Optimistic Locking

We should remember that for versioned entities optimistic locking is available by default. Yet there are several ways of requesting it explicitly.

6.1. Find

To request optimistic locking we can pass the proper LockModeType as an argument to find method of EntityManager:

entityManager.find(Student.class, studentId, LockModeType.OPTIMISTIC);

6.2. Query

Another way to enable locking is using the setLockMode method of Query object:

Query query = entityManager.createQuery("from Student where id = :id"); query.setParameter("id", studentId); query.setLockMode(LockModeType.OPTIMISTIC_INCREMENT); query.getResultList()

6.3. Explicit Locking

We can set a lock by calling EnitityManager's lock method:

Student student = entityManager.find(Student.class, id); entityManager.lock(student, LockModeType.OPTIMISTIC);

6.4. Refresh

We can call the refresh method the same way as the previous method:

Student student = entityManager.find(Student.class, id); entityManager.refresh(student, LockModeType.READ);

6.5. NamedQuery

The last option is to use @NamedQuery with the lockMode property:

@NamedQuery(name="optimisticLock", query="SELECT s FROM Student s WHERE s.id LIKE :id", lockMode = WRITE)

7. OptimisticLockException

When the persistence provider discovers optimistic locking conflicts on entities, it throws OptimisticLockException. We should be aware that due to the exception the active transaction is always marked for rollback.

It's good to know how we can react to OptimisticLockException. Conveniently, this exception contains a reference to the conflicting entity. However, it's not mandatory for the persistence provider to supply it in every situation. There is no guarantee that the object will be available.

There is a recommended way of handling the described exception, though. We should retrieve the entity again by reloading or refreshing. Preferably in a new transaction. After that, we can try to update it once more.

8. Conclusion

In this tutorial, we got familiar with a tool which can help us orchestrate concurrent transactions. Optimistic locking uses version attributes included in entities to control concurrent modifications on them.

Therefore, it ensures that any updates or deletes won't be overwritten or lost silently. Opposite to pessimistic locking, it doesn't lock entities on the database level and consequently, it isn't vulnerable to DB deadlocks.

Hemos aprendido que el bloqueo optimista está habilitado para las entidades versionadas de forma predeterminada. Sin embargo, hay varias formas de solicitarlo explícitamente utilizando varios tipos de modos de bloqueo.

Otro hecho que debemos recordar es que cada vez que hay actualizaciones en conflicto en las entidades, debemos esperar una OptimisticLockException .

Por último, el código fuente de este tutorial está disponible en GitHub.