O Hibernate utiliza um mecanismo chamado Verificação de Sujeira (Dirty Checking) para determinar quais entidades foram modificadas e precisam ser atualizadas no banco de dados. Esse processo ocorre no momento da submissão da transação.
Quando um objeto principal (entidade pai) contém objetos associados (entidades filhas), o Hibernate rastreia as alterações tanto no objeto principal quanto em seus componentes associados. Se qualquer propriedade de uma entidade associada for modificada, o Hibernate a marcará como "suja" e gerará um SQL de atualização correspondente.
Considere o seguinte exemplo com entidades relacionadas:
@Entity
public class Produto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
private Double preco;
@OneToOne(cascade = CascadeType.ALL)
private DetalhesProduto detalhes;
// Getters e setters
}
@Entity
public class DetalhesProduto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String descricao;
private String fabricante;
// Getters e setters
}
Ao carregar uma instância de Produto e modificar suas propriedades ou as de seu DetalhesProduto associado, o Hibernate detectará essas mudanças:
Produto produto = entityManager.find(Produto.class, 1L);
produto.setNome("Novo Nome");
produto.getDetalhes().setDescricao("Descrição Atualizada");
// Ao final da transação, o Hibernate gerará UPDATEs para ambas as tabelas.
Essa funcionalidade de verificação de sujeira é automática e conveniente para manter a consistência dos dados.
Desafios em Ambientes Multithread
O cenário se complica em aplicações multithread. Se múltiplas threads acessam e modificam a mesma entidade simultaneamente, sem mecanismos de sincronização adequados, podem ocorrer Condições de Corrida (Race Conditions). Como a verificação de sujeira do Hibernate ocorre na submissão da transação, a ordem em que as transações são submetidas pode levar à perda de atualizações. A última transação a ser submetida pode sobrescrever as modificações feitas por outras, resultando em inconsistência de dados.
Estratégias para Concorrência
Para mitigar problemas de concorrência em ambientes multithread, diversas abordagens podem ser empregadas:
1. Bloqueio Otimista (Optimistic Locking)
O bloqueio otimista gerencia a concorrência sem a necessidade de bloquear explicitamente os recursos. Isso é tipicamente implementado com um campo de versão na entidade, anotado com @Version. Quando uma entidade é atualizada, seu número de versão é incrementado. Antes de confirmar a atualização, o Hibernate verifica se a versão no banco de dados corresponde à versão carregada. Se não corresponder, significa que outro thread modificou a entidade, e uma OptimisticLockException é lançada.
@Entity
public class Recurso {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String dados;
@Version
private Long numeroVersao;
// Getters e setters
}
2. Bloqueio Pessimista (Pessimistic Locking)
O bloqueio pessimista assume que conflitos de concorrência são prováveis e utiliza bloqueios para impedir que outros threads acessem ou modifiquem um recurso enquanto ele está sendo processado. No Hibernate, isso pode ser feito ao consultar a entidade, especificando um modo de bloqueio, como LockModeType.PESSIMISTIC_WRITE. Isso garante que nenhum outro transaction possa modificar a entidade até que a transação atual seja concluída.
Recurso recurso = entityManager.find(Recurso.class, idRecurso, LockModeType.PESSIMISTIC_WRITE);
// Realize as modificações no recurso aqui.
// A transação manterá o bloqueio até o commit/rollback.
3. Sincronização de Código Java
Para garantir que apenas um thread por vez possa modificar um objeto, os mecanismos de sincronização nativos do Java, como a palavra-chave synchronized, podem ser utilizados. Ao aplicar synchronized a um bloco de código que acessa e modifica a entidade, você garante a exclusividade do acesso, prevenindo condições de corrida.
synchronized (objetoEntidade) {
// Código para modificar objetoEntidade
}
A escolha da estratégia mais adequada depende dos requisitos específicos da aplicação, como o nível de concorrência esperado, a sensibilidade dos dados e as implicações de desempenho. Bloqueio otimista é eficiente para alta concorrência, enquanto bloqueio pessimista é mais seguro para dados críticos, embora possa impactar a performance. A sincronização Java deve ser usada com cautela para evitar gargaols e deadlocks.