Bloqueio pessimista na APP
1. Visão geral
Existem muitas situações em que queremos recuperar dados de um banco de dados. Às vezes, queremos bloqueá-lo para processamento adicional, para que ninguém mais possa interromper nossas ações.
Podemos pensar em dois mecanismos de controle de simultaneidade que nos permitem fazer isso: definir o nível de isolamento de transação adequado ou definir um bloqueio nos dados de que precisamos no momento.
O isolamento da transação é definido para conexões com o banco de dados. Podemos configurá-lo para reter os diferentes graus de bloqueio de dados.
No entanto,the isolation level is set once the connection is createde afeta todas as instruções dentro dessa conexão. Felizmente, podemos usar o bloqueio pessimista, que usa mecanismos de banco de dados para reservar acesso exclusivo mais granular aos dados.
Podemos usar um bloqueio pessimista para garantir que nenhuma outra transação possa modificar ou excluir dados reservados.
Existem dois tipos de bloqueios que podemos reter: um bloqueio exclusivo e um bloqueio compartilhado. Poderíamos ler, mas não gravar dados, quando outra pessoa tiver um bloqueio compartilhado. Para modificar ou excluir os dados reservados, precisamos ter um bloqueio exclusivo.
Podemos adquirir bloqueios exclusivos usando as instruções ‘SELECT … FOR UPDATE’.
2. Modos de bloqueio
A especificação JPA define três modos de bloqueio pessimistas que iremos discutir:
-
PESSIMISTIC_READ - nos permite obter um bloqueio compartilhado e evitar que os dados sejam atualizados ou excluídos
-
PESSIMISTIC_WRITE - nos permite obter um bloqueio exclusivo e evitar que os dados sejam lidos, atualizados ou excluídos
-
PESSIMISTIC_FORCE_INCREMENT - funciona comoPESSIMISTIC_WRITE e adicionalmente incrementa um atributo de versão de uma entidade com versão
Todos eles são membros estáticos da classeLockModeType e permitem que as transações obtenham um bloqueio de banco de dados. Todos eles são retidos até que a transação seja confirmada ou revertida.
É importante notar que podemos obter apenas um bloqueio de cada vez. Se for impossível, umPersistenceException é lançado.
2.1. PESSIMISTIC_READ
Sempre que quisermos apenas ler os dados e não encontrar leituras sujas, podemos usarPESSIMISTIC_READ (bloqueio compartilhado). We won’t be able to make any updates or deletes though.
Às vezes acontece que o banco de dados que usamos não suporta o bloqueioPESSIMISTIC_READ, então é possível que obtivemos o bloqueioPESSIMISTIC_WRITE.
2.2. PESSIMISTIC_WRITE
Qualquer transação que precise adquirir um bloqueio de dados e fazer alterações deve obter o bloqueioPESSIMISTIC_WRITE. De acordo com a especificaçãoJPA, manter o bloqueioPESSIMISTIC_WRITE impedirá que outras transações leiam, atualizem ou excluam os dados.
Observe que alguns sistemas de banco de dados implementammulti-version concurrency control, que permite aos leitores buscar dados que já foram bloqueados.
2.3. PESSIMISTIC_FORCE_INCREMENT
Este bloqueio funciona de forma semelhante aPESSIMISTIC_WRITE, mas foi introduzido para cooperar com entidades versionadas - entidades que têm um atributo anotado com@Version.
Quaisquer atualizações de entidades com versão podem ser precedidas da obtenção do bloqueioPESSIMISTIC_FORCE_INCREMENT. Acquiring that lock results in updating the version column.
Cabe a um provedor de persistência determinar se ele suportaPESSIMISTIC_FORCE_INCREMENT para entidades não versionadas ou não. Se não, ele lança oPersistanceException.
2.4. Exceções
É bom saber qual exceção pode ocorrer ao trabalhar com bloqueio pessimista. A especificaçãoJPA fornece diferentes tipos de exceções:
-
PessimisticLockException - indica que a obtenção de um bloqueio ou a conversão de um bloqueio compartilhado em exclusivo falha e resulta em uma reversão no nível da transação
-
LockTimeoutException – indica que obter um bloqueio ou converter um bloqueio compartilhado em exclusivo atinge o tempo limite e resulta em uma reversão no nível de instrução
-
PersistanceException – indica que ocorreu um problema de persistência. PersistanceException e seus subtipos, excetoNoResultException,NonUniqueResultException,LockTimeoutException eQueryTimeoutException, marks the active transaction to be rolled back.
3. Usando bloqueios pessimistas
There are a few possible ways to configure a pessimistic lock on a single record or group of records. Vamos ver como fazer no JPA.
3.1. Find
Provavelmente é a maneira mais direta. Basta passar um objetoLockModeType como parâmetro para o métodofind:
entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);
3.2. Inquerir
Além disso, podemos usar um objetoQuery também e chamar o settersetLockMode com um modo de bloqueio como parâmetro:
Query query = entityManager.createQuery("from Student where studentId = :studentId");
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
query.getResultList()
3.3. Bloqueio explícito
Também é possível bloquear manualmente os resultados recuperados pelo método find:
Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.lock(resultStudent, LockModeType.PESSIMISTIC_WRITE);
3.4. Atualizar
Se quisermos sobrescrever o estado da entidade pelo métodorefresh , também podemos definir um bloqueio:
Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.refresh(resultStudent, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
3.5. NamedQuery
A anotação@NamedQuery nos permite definir um modo de bloqueio também:
@NamedQuery(name="lockStudent",
query="SELECT s FROM Student s WHERE s.id LIKE :studentId",
lockMode = PESSIMISTIC_READ)
4. Âmbito de bloqueio
Lock scope parameter defines how to deal with locking relationships of the locked entity. É possível obter um bloqueio apenas em uma única entidade definida em uma consulta ou, adicionalmente, bloquear seus relacionamentos.
Para configurar o escopo, podemos usarPessimisticLockScope enum. Ele contém dois valores:NORMALeEXTENDED.
Podemos definir o escopo passando um parâmetro 'javax.persistance.lock.scope' com o valorPessimisticLockScope como um argumento para o método apropriado deEntityManager,Query,TypedQuery ouNamedQuery:
Map properties = new HashMap<>();
map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED);
entityManager.find(
Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);
4.1. PessimisticLockScope.NORMAL
We should know that the PessimisticLockScope.NORMAL is the default scope. Com este escopo de bloqueio, bloqueamos a própria entidade. Quando usado com herança unida, também bloqueia os ancestrais.
Vejamos o código de amostra com duas entidades:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
@Id
private Long id;
private String name;
private String lastName;
// getters and setters
}
@Entity
public class Employee extends Person {
private BigDecimal salary;
// getters and setters
}
Quando queremos obter um bloqueio emEmployee, podemos observar a consultaSQL que abrange essas duas entidades:
SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY
FROM PERSON t0, EMPLOYEE t1
WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE
4.2. PessimisticLockScope.EXTENDED
O escopoEXTENDED cobre a mesma funcionalidade deNORMAL. Além disso,it’s able to block related entities in a join table.
Simplificando, ele funciona com entidades anotadas com@ElementCollection ou@OneToOne,@OneToMany etc. com@JoinTable.
Vejamos o código de amostra com a anotação@ElementCollection:
@Entity
public class Customer {
@Id
private Long customerId;
private String name;
private String lastName;
@ElementCollection
@CollectionTable(name = "customer_address")
private List addressList;
// getters and setters
}
@Embeddable
public class Address {
private String country;
private String city;
// getters and setters
}
Vamos analisar algumas consultas ao pesquisar a entidadeCustomer:
SELECT CUSTOMERID, LASTNAME, NAME
FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE
SELECT CITY, COUNTRY, Customer_CUSTOMERID
FROM customer_address
WHERE (Customer_CUSTOMERID = ?) FOR UPDATE
Podemos ver que há duas consultas ‘FOR UPDATE’ que bloqueiam uma linha na tabela do cliente, bem como uma linha na tabela de junção.
Outro fato interessante que devemos estar cientes é que nem todos os provedores de persistência oferecem suporte a escopos de bloqueio.
5. Definindo o tempo limite de bloqueio
Além de definir os escopos de bloqueio, podemos ajustar outro parâmetro de bloqueio - timeout. The timeout value is the number of milliseconds that we want to wait for obtaining a lock until the LockTimeoutException occurs.
Podemos alterar o valor do tempo limite de forma semelhante aos escopos de bloqueio, usando a propriedade ‘javax.persistence.lock.timeout' com o número adequado de milissegundos.
Também é possível especificar o bloqueio "sem espera" alterando o valor de tempo limite para zero. No entanto, devemos ter em mente que existem drivers de banco de dados quedon’t support setting a timeout value this way.
Map properties = new HashMap<>();
map.put("javax.persistence.lock.timeout", 1000L);
entityManager.find(
Student.class, 1L, LockModeType.PESSIMISTIC_READ, properties);
6. Conclusão
Quando definir o nível de isolamento adequado não é suficiente para lidar com transações simultâneas, a JPA nos fornece um bloqueio pessimista. Ele nos permite isolar e orquestrar diferentes transações para que não acessem o mesmo recurso ao mesmo tempo.
Para conseguir isso, podemos escolher entre os tipos discutidos de bloqueios e, consequentemente, modificar parâmetros como seus escopos ou tempos limites.
Por outro lado, devemos lembrar que entender os bloqueios do banco de dados é tão importante quanto entender os mecanismos dos sistemas de banco de dados subjacentes. Também é importante ter em mente que o comportamento de bloqueios pessimistas depende do provedor de persistência com quem trabalhamos.
Por fim, o código-fonte deste tutorial está disponível no GitHub parahibernateeEclipseLink.