Criando Sistemas de Reservas Com Versionless Optimistic Locking, Spring Boot e JPA/Hibernate
Tags: java, jpa, concurrency, spring
Published: 2022-08-17 22:00:00

Como visto no [artigo anterior](https://dev.to/jordihofc/criando-sistemas-de-reservas-consistentes-com-optimistic-locking-spring-boot-e-jpahibernate-2h8b) o **_Optmistic Locking_** é uma opção para lidar com [Race Conditions](https://vladmihalcea.com/race-condition/) para cenários com baixo e/ou moderado índice de conflitos. O principal motivo para o uso de bloqueio otimista nestes cenários é porque não haverá um número exagerado de transações conflitantes e o seu banco de dados (BD) não será sobrecarregado com uma alta quantidade de **_rollbacks_**.

Caso seu sistema necessite atualizar atributos distintos de uma entidade simultaneamente o **_Optmistic Locking_** poderá se tornar um empecilho dado que o mecanismo de bloqueio otimista  encara as atualizações como tudo ou nada, ou seja, caso a versão da entidade em memória não corresponda a presente ao BD a transação será abortada independente se os campos atualizados não sejam sobrepostos.

Para entender melhor, dado a entidade Mesa descrita no código abaixo.

```JAVA
@Entity
public class Mesa {
    @Id
    @GeneratedValue
    private Long id;
    @Column(nullable = false)
    private Integer quantidadeDeLugares;
    @Column(nullable = false)
    private boolean disponivelParaReserva;
    @Version
    private Long version;
    //construtores e metodos omitidos
}
```

Imagine que duas transações simultâneas desejem atualizar os atributos `quantidadeDeLugares` e `disponivelParaReserva`. Ambas consultem o registro e recebam a mesma versão da entidade `Mesa`. A primeira transação (**_Tx1_**) atualiza a quantidade de lugares, fazendo com que a versão da mesa seja incrementada. Enquanto a segunda transação (**_Tx2_**) tenta atualizar o status da mesa para indisponível para reserva, porém, dado que **_Tx1_**  incrementou a versão da entidade, agora a versão está obsoleta, portanto, **_Tx2_** sera abortada. 


![Conflito em atualizações com atributos não sobrepostos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/149cnmgmcv3unqirpzzr.png)

Em casos de uso onde é necessário que atualizações com atributos não sobrepostos não conflitem, ou quando alterar o schema de dados para inserir um atributo de versão seja caro independente do motivo o Hibernate oferece suporte ao controle de concorrência com mecanismo de **_Versionless Optmistic Locking_** ou bloqueio otimista sem versão em português.


## **_Version-less optimistic locking_**

Os bloqueios otimistas estão relacionados a um atributo incremental para controle de versão da entidade, e é através deste que a integridade e consistência são mantidas em atualizações concorrentes. 

Existem sistemas onde o custo de utilizar uma estratégia de **_Optimistic Locking_** é alto, pois não é simples inserir uma nova coluna na tabela dado que o impacto desta mudança poderá causar anomalias em outras funcionalidades do sistema, ou para realizar mudanças no _schema_ exija aprovações em um sistema altamente burocrático, ou você trabalha em um sistema legado onde alterar o schema não é uma opção. 

Para estes casos é possível criar um versionamento da entidade a partir do próprio estado do recurso, seja através de uma coluna atualizada ou da junção de todas colunas.

### **_Versionless Optmistic Locking_** com Hibernate

Hibernate oferece suporte ao controle de concorrência com bloqueio otimista sem versão, para habilitar o mesmo devemos utilizar anotação `@OptimisticLocking` ao nível de entidade, esta anotação recebe como argumento a estratégia de bloqueio otimista descrita por um `OptimisticLockType`.

O Hibernate oferece suporte as seguintes estratégias de bloqueio otimista.

- `OptimisticLockType.NONE`: indica que o mecanismo de bloqueio otimista está desabilitado;
- `OptimisticLockType.VERSION`: indica que o mecanismo de bloqueio otimista utilizará o atributo anotado com `@Version` como versão da entidade;
- `OptimisticLockType.DIRTY`: indica que o mecanismo de bloqueio otimista utilizará apenas os atributos atualizados pela transação como versão da entidade;
- `OptimisticLockType.ALL`: indica que o mecanismo de bloqueio otimista utilizar todos os atributos da entidade como versão;

Como o mecanismo de controle de concorrência precisa inserir condições adicionais em sua `Query` de atualização, é necessário que a entidade seja anotado com `@DynamicUpdate` ao nível de entidade.

```JAVA
@Entity
@OptimisticLocking(type = OptimisticLockType.ALL)
@DynamicUpdate
public class Mesa {
    @Id
    @GeneratedValue
    private Long id;
    //Demais campos, construtores e metodos omitidos  
}
```

####  Controle de Concorrência com `OptimisticLockType.ALL`

Dado que a estratégia de bloqueio otimista `OptimisticLockType.ALL` esteja habilitada no momento de uma atualização o Hibernate irá verificar se o estado de cada atributo da entidade corresponde em memória ao estado presente no BD.

Para melhor entendimento observe o código abaixo.

```JAVA
@Transactional
public void atualizarQuantidadeDeLugares(Long mesaId, int qtdLugares){
   Mesa mesa = entityManager.find(Mesa.class, mesaId);
   mesa.setQuatidadeDeLugares(qtdLugares);
}
```
Hibernate gera o seguinte **_SQL_**: 

```SQL
   SELECT *
     FROM mesa m
    WHERE m.id = :mesaId

   UPDATE mesa
      SET quantidadeLugares = :qtdLugares
    WHERE id = :medaId 
      AND quantidadeLugares = 2
      AND disponivelParaReserva = true
```

No SQL acima é possível visualizar que todos os campos se uniram e convergiram em um único atributo de versão global. Esta estratégia é utilizada quando é impossível inserir um atributo para versionamento da entidade. Em contrapartida, as atualizações com atributos não sobrepostos conflitam. 

####  Controle de Concorrência com `OptimisticLockType.DIRTY`

A estratégia de bloqueio otimista `OptimisticLockType.DIRTY` favorece que apenas os campos "sujos" na atualização sejam  utilizados como versão da entidade, ou seja, agora atualizações com atributos não sobrepostos não conflitam mais.

Observe o comportamento do seguinte método de atualização.

```JAVA
@Transactional
public void atualizarQuantidadeDeLugares(Long mesaId, int qtdLugares){
   Mesa mesa = entityManager.find(Mesa.class, mesaId);
   mesa.setQuatidadeDeLugares(qtdLugares);
}
```
Hibernate gera o seguinte **_SQL_**: 

```SQL
   SELECT *
     FROM mesa m
    WHERE m.id = :mesaId

   UPDATE mesa
      SET quantidadeLugares = :qtdLugares
    WHERE id = :medaId 
      AND quantidadeLugares = 2
```
O **_SQL_** apresentado acima demostra que apenas o atributo `quantidadeLugares` foi utilizado na condição do **`WHERE`**, ou seja, quando a estratégia de bloqueio otimista `DIRTY` é utilizada apenas os campos atualizados são utilizados como versão. O sistema permite que atualizações a atributos distintos não conflitem evitando **`OptimisticLockException`**.

![Atualizações não sobrepostas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9honl8lb78d0h0ktf58n.png)
 
O diagrama acima pode ser resumido em:

1. A primeira transação (**_Tx1_**) realiza a consulta de leitura da entidade Mesa com id igual a 1. 
2. A segunda transação (**_Tx2_**) simultaneamente também realiza a consulta de leitura da entidade Mesa com id igual a 1
3. **_Tx1_** altera a quantidade de lugares na mesa.
4. **_Tx2_** altera o status de disponibilidade da mesa

## Construindo uma API REST para reserva de mesas com **_Versionless Optimistic Locking_**

Ao nível de **_controller_** não existem mudanças a [implementação do artigo anterior](https://dev.to/jordihofc/criando-sistemas-de-reservas-consistentes-com-optimistic-locking-spring-boot-e-jpahibernate-2h8b) dado que nossas mudanças afetam apenas as entidades. 

```JAVA
@PostMapping("/mesas/{id}/reservas")
@Transactional
public ResponseEntity<?> reservar(
        @PathVariable(value = "id") Long mesaId,
        @RequestBody ReservaMesaRequest request,
        UriComponentsBuilder uriBuilder
) {
    Mesa mesa = mesaRepository.findById(mesaId)
            .orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Mesa não cadastrada"));

    Usuario usuario = usuarioRepository.findById(request.getUsuarioId())
            .orElseThrow(() -> new ResponseStatusException(UNPROCESSABLE_ENTITY, "Usuario não cadastrado"));

    LocalDateTime dataDaReserva = request.getDataReserva();

    if (reservaRepository.existsMesaByIdAndReservadoParaIs(mesaId, dataDaReserva)) {
        throw new ResponseStatusException(UNPROCESSABLE_ENTITY, "Horario indisponivel para reserva");
    }

    Reserva reserva = mesa.reservar(usuario, dataDaReserva);
    reservaRepository.save(reserva);


    URI location = uriBuilder.path("/mesas/{id}/reservas/{reservaId}")
            .buildAndExpand(mesa.getId(), reserva.getId())
            .toUri();

    return ResponseEntity.created(location).build();
}
```
Independente da abordagem de bloqueio otimista sem versão é indispensável a construção de um Exception Handler para `ObjectOptimisticLockingFailureException`, quando atualizações conflitarem é essencial que a resposta contenham um `Http Status Conflict`.

```JAVA
@RestControllerAdvice
public class DeafultExceptionHandler {    
 @ExceptionHandler(ObjectOptimisticLockingFailureException.class)
    public ResponseEntity<?> optmisticLock(ObjectOptimisticLockingFailureException ex) {

        String msg = "Aconteceu um conflito em sua atualização, tente novamente.";

        return status(409)
                .body(
                        Map.of("mensagem",msg)
                );
    }
}
```


## Conclusão

Construir mecanismo para controle de concorrência é uma tarefa complexa dado que cada sistema possui suas restrições. O mecanismo de **_Versionless Optimistic Locking_** do Hibernate oferece flexibilidade para implementação da estratégia de detecção de conflitos para diversos casos de uso.

Caso o conflito de atualizações estejam causando integridade aos dados em um sistema legado ou quando alterar o _schema_ seja custoso o `OptimisticLockType.ALL` se torna uma ótima solução dado que seu comportamento é semelhante ao **_Optimistic Locking_** tradicional.

Porém, se existe a necessidade de que atualizações a atributos não sobrepostos não conflitem a estratégia de `OptimisticLockType.DIRTY` se torna uma solução já que apenas os atributos "sujos" são utilizados como versão da entidade.

## Referências

- [How to prevent OptimisticLockException with Hibernate versionless optimistic locking](https://vladmihalcea.com/how-to-prevent-optimisticlockexception-using-hibernate-versionless-optimistic-locking/)

- [Chapter: Concurrency Control - High-Performance Java Persistence
Book](https://vladmihalcea.com/books/high-performance-java-persistence/?utm_source=blog&utm_medium=banner&utm_campaign=banner)