O Hibernate oferece mecanismos robustos para lidar com problemas de concorrência em aplicações, principalmente através de estratégias de bloqueio de dados. Essas estratégias visam garantir a integridade dos dados em cenários onde múltiplos usuários ou processos podem tentar acessar e modificar as mesmas informações simultaneamente. O Hibernate divide essas estratégias em duas categorias principais: bloqueio otimista e bloqueio pessimista.
Bloqueio Otimista
O bloqueio otimista parte do princípio de que conflitos de modificação simultânea são raros. Em vez de bloquear os dados preventivamente, ele permite que as transações leiam e modifiquem os dados livremente. A verificação de conflitos ocorre no momento da escrita. Se, durante a tentativa de atualização, o sistema detectar que os dados foram alterados por outra transação desde a leitura, um erro é lançado, impedindo a sobrescrita e notificando o usuário sobre a ocorrência de um conflito.
Tipicamente, o bloqueio otimista é implementado utilizando um campo de controle, como um número de versão ou um timestamp, associado a cada registro. Ao ler um registro, a versão atual é capturada. Ao tentar salvar as modificações, o Hibernate compara a versão capturada com a versão presente no banco de dados. Se ambas forem iguais, a atualização prossegue e a versão é incrementada. Caso contrário, significa que o registro foi modificado por outra transação, e uma exceção é lançada.
Implementação com Campo de Versão
Para habilitar o bloqueio otimista com controle de versão no Hibernate, basta adicionar um campo de inteiro (ou similar) à sua entidade e anotá-lo com @Version.
import jakarta.persistence.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Entity
@Table(name = "t_book", schema = "test", catalog = "")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Version
@Column(name = "version")
private int recordVersion; // Nome da variável alterado para evitar semelhança
@Basic
@Column(name = "name")
private String title; // Nome da coluna e variável alterados
@Basic
@Column(name = "author")
private String authorName; // Nome da variável alterado
@Basic
@Column(name = "publish_date")
private Date publicationDate; // Nome da variável alterado
@Basic
@Column(name = "price")
private BigDecimal cost; // Nome da variável alterado
@Transient
private Integer catalogIdentifier; // Nome da variável alterado
}
Exemplo de Teste para Bloqueio Otimista
O teste a seguir demonstra como um conflito de bloqueio otimista pode ser detectado. Se você interromper a execução após a leitura e modificar manualmente o campo recordVersion no banco de dados, a próxima operação de atualização lançará uma exceção.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.hibernate.criterion.Restrictions;
import java.math.BigDecimal;
@SpringBootTest
public class OptimisticLockTest {
@Autowired
private SessionFactory sessionFactory;
private Session currentSession;
@BeforeEach
public void setup() {
this.currentSession = sessionFactory.getCurrentSession();
}
@Test
@Transactional
public void testOptimisticConflictDetection() {
// Consulta para encontrar o livro específico
Query<book> query = currentSession.createCriteria(Book.class)
.add(Restrictions.eq("title", "O Guia do Mochileiro das Galáxias"))
.uniqueResult();
Book bookToUpdate = query.getSingleResult();
// Modifica um campo do livro
bookToUpdate.setCost(new BigDecimal("99.90"));
// Ponto de interrupção sugerido:
// Neste ponto, se o 'recordVersion' do registro no banco de dados for modificado manualmente
// (por outra transação ou ferramenta), a linha abaixo lançará uma OptimisticLockException.
currentSession.update(bookToUpdate);
currentSession.flush(); // Força a escrita no banco de dados
System.out.println("Livro: " + bookToUpdate.getTitle() + ", Novo Preço: " + bookToUpdate.getCost());
}
}
</book>
A instrução SQL gerada internamente pelo Hibernate para a operação de atualização se parecerá com:
UPDATE t_book
SET author=?, catalog_identifier=?, name=?, price=?, publish_date=?, version=?
WHERE id=? AND version=?
Note a inclusão da condição version=? na cláusula WHERE, que é crucial para a lógica do bloqueio otimista.
Bloqueio Pessimista
O bloquieo pessimista, em contraste, assume que conflitos são prováveis e age preventivamente. Ele utiliza mecanismos de bloqueio nativos do banco de dados para impedir que outras transações modifiquem os dados enquanto uma transação os está utilizando. Essa abordagem garante que os dados permaneçam inalterados até que a transação atual os libere.
Tipos de Bloqueio Pessimista
O bloqueio pessimista se manifesta principalmente em duas formas:
- Bloqueio de Escrita (Exclusivo): Conhecido como
LockMode.PESSIMISTIC_WRITE. Quando uma transação adquire um bloqueio de escrita em um registro, apenas essa transação pode ler e modificar o registro. Nenhuma outra transação pode adquirir qualquer tipo de bloqueio sobre esse registro até que o bloqueio original seja liberado. Em SQL, isso é frequentemente implementado com a cláusulaFOR UPDATE. - Bloqueio de Leitura (Compartilhado): Conhecido como
LockMode.PESSIMISTIC_READ. Uma transação que adquire um bloqueio de leitura permite que outras transações também leiam o mesmo registro (adquirindo bloqueios de leitura compartilhados). No entanto, nenhuma outra transação pode adquirir um bloqueio de escrita enquanto os bloqueios de leitura estiverem ativos. Em SQL, isso pode ser mapeado paraLOCK IN SHARE MODE.
Implementação de Bloqueio Pessimista
A implementação do bloqueio pessimista no Hibernate é realizada através do método get() (ou métodos equivalentes de consulta) ao especificar o modo de bloqueio desejado.
Bloqueio de Escrita (Exclusivo)
Ao utilizar LockMode.PESSIMISTIC_WRITE, o bloqueio é adquirido no momento em que a consulta é executada e mantido até o final da transação atual. Durante esse período, outras transações que tentarem modificar os mesmos dados ficarão bloquedaas.
import org.hibernate.LockMode;
// ... outros imports
// ... dentro de um método de teste ou serviço transacional
Session session = sessionFactory.getCurrentSession();
Criteria criteria = session.createCriteria(Book.class);
criteria.setLockMode(LockMode.PESSIMISTIC_WRITE); // Define o bloqueio de escrita
criteria.add(Restrictions.eq("title", "O Nome do Vento"));
Book lockedBook = (Book) criteria.uniqueResult();
// Ponto de interrupção sugerido:
// Tente modificar o registro "O Nome do Vento" no banco de dados neste ponto.
// A operação ficará bloqueada até que a transação atual seja concluída.
System.out.println("Livro bloqueado para escrita: " + lockedBook.getTitle());
// ... outras operações da transação ...
A consulta SQL gerada para esta operação incluirá a cláusula FOR UPDATE:
SELECT
this_.id as id1_0_0_, this_.author as author2_0_0_, this_.catalog_identifier as catalog_7_0_0_,
this_.name as name3_0_0_, this_.price as price4_0_0_, this_.publish_date as publish_5_0_0_,
this_.version as version6_0_0_
FROM
t_book this_
WHERE
this_.name=? FOR UPDATE
Bloqueio de Leitura (Compartilhado)
Com LockMode.PESSIMISTIC_READ, o bloqueio de leitura é aplicado durante a execução da consulta. A principal diferença é que outras transações podem ler os mesmos dados concorrentemente, mas não podem modificá-los. O bloqueio é liberado assim que a consulta retorna os resultados, o que significa que ele não protege contra modificações que ocorram *após* a leitura, mas impede modificações *durante* a leitura por outras transações.
import org.hibernate.LockMode;
// ... outros imports
// ... dentro de um método de teste ou serviço transacional
Session session = sessionFactory.getCurrentSession();
Criteria criteria = session.createCriteria(Book.class);
criteria.setLockMode(LockMode.PESSIMISTIC_READ); // Define o bloqueio de leitura
criteria.add(Restrictions.eq("title", "Duna"));
Book sharedBook = (Book) criteria.uniqueResult();
// Neste ponto, o bloqueio de leitura foi aplicado e liberado.
// Outras transações podem agora modificar o registro se desejarem,
// desde que não estejam tentando adquirir um bloqueio de escrita que conflite com um bloqueio de leitura ativo.
System.out.println("Livro lido com bloqueio compartilhado: " + sharedBook.getTitle());
// ... outras operações da transação ...
A consulta SQL correspondente utilizará a cláusula LOCK IN SHARE MODE (ou similar, dependendo do dialeto SQL):
SELECT
this_.id as id1_0_0_, this_.author as author2_0_0_, this_.catalog_identifier as catalog_7_0_0_,
this_.name as name3_0_0_, this_.price as price4_0_0_, this_.publish_date as publish_5_0_0_,
this_.version as version6_0_0_
FROM
t_book this_
WHERE
this_.name=? LOCK IN SHARE MODE
A escolha entre bloqueio otimista e pessimista depende dos requisitos específicos da aplicação, da frequência esperada de conflitos e das características de desempenho desejadas.