Hibernate Flush: Write-behind
Veja neste artigo como utilizar o Flush do Hibernate em conjunto com sua técnica write-behind.
Neste artigo veremos um recurso muito importante do Hibernate: o Flush. Você entenderá com detalhes como este recurso funciona e como utilizá-lo da maneira correta.
O Hibernate possui uma técnica chamada “write-behind” que faz com que todas as operações com o banco sejam realizadas o mais tarde possível, para tentar aumentar a performance da aplicação. Em outra palavras, quando você realiza uma operação de DELETE, por exemplo, não necessariamente ela será executada naquele determinado instante, mas o Hibernate irá considerar alguns aspectos para definir quando sua operação deverá ser executada, Estes aspectos veremos mais adiante.
O Flush nada mais é do que a forma com que o Hibernate propaga as alterações da sua aplicação com o banco. Como falamos anteriormente, a técnica “write-behind” faz com que as operações no banco não sejam realizadas pontualmente, fazendo assim com que os dados da aplicação possam ficar diferentes dos dados presentes no banco de dados, Sendo assim, o “flush” irá sincronizar os dados da aplicação com o banco de dados. Na verdade, o que o Hibernate faz é executar todas as operações necessárias (INSERTS,DELETES, UPDATES …).
Mas em que momento o Flush é executado? Existem alguns momentos específicos em que o Hibernate realiza um Flush automático, a fim de sincronizar estes dados com o banco, são eles:
- Quando a transação sofre um 'commit';
- Antes de um query ser executada;
- Quando chamamos explicitamente o método “flush()”.
Então baseado nas três situações acima você saberá quando e porque um flush() está sendo executado, podendo evitá-lo ou simplesmente monitorá-lo. Vamos ver nas seções posteriores exemplos práticos do flush() funcionando.
Exemplos práticos de flush() no Hibernate
Vamos primeiramente mapear nossa classe Client que servirá para todos os nossos exemplos, conforme a Listagem 1.
Listagem 1. Classe Client
@Entity
public class Client {
@Id @GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Column(unique=true,nullable=false)
private String personalNumber;
@Column
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPersonalNumber() {
return personalNumber;
}
public void setPersonalNumber(String personalNumber) {
this.personalNumber = personalNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Nossa classe Client tem um ID que é gerado automaticamente e temos também um “personalNumber” que deve ser sempre único, pois identifica um cliente na nossa aplicação, algo como um token para cada cliente, e o nome do cliente. Vamos agora criar um DAO para realizar as operações com o banco de dados, conforme a Listagem 2.
Listagem 2. DAO do Client
import java.util.Collection;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
public class ClientDao{
@PersistenceContext
private EntityManager entityManager;
//Insere um cliente no banco
public Client createClient(Client client) {
entityManager.persist(client);
return client;
}
/*
* Busca por um cliente através do personalNumber, caso o
* personalNumber não seja encontrado
* então retorna NULL.
* */
@SuppressWarnings("unchecked")
public Client loadClientWithPersonalNumberOrReturnNull
(String personalNumber) {
Collection<Client> clients = (Collection<Client>)
entityManager.createQuery("select c from Client c
where c.personalNumber = :personalNumber")
.setParameter("personalNumber",
personalNumber).getResultList();
if (clients.size()==0)
{
return null;
}
if (clients.size()>1)
{
throw new IncorrectResultSizeDataAccessException
("There are "+clients.size()+" client with personal
number "+personalNumber,1,clients.size());
}
Client client = clients.iterator().next();
client.getAddresses().size();
client.getAccounts().size();
return client;
}
//Remove um cliente do banco através do seu ID
public void deleteClient(long clientId) {
entityManager.remove(entityManager.getReference
(Client.class, clientId));
}
}
Só entenderemos toda a estrutura acima quando virmos uma sequência de comandos que serão executados a seguir para então explicarmos como o flush() atua nessa sequência. Observe a Listagem 3.
Listagem 3. Sequência de Comandos
Client client = new Client(PERSONAL_NUM);
LOG.debug("Creating first client");
clientDao.createClient(client);
LOG.debug("Changing first client");
client.setName("Carl von Bahnhof");
LOG.debug("Deleting first client");
clientDao.deleteClient(client.getId());
LOG.debug("Trying to load first client");
assertNull(clientDao
.loadClientWithPersonalNumberOrReturnNull
(PERSONAL_NUM));
LOG.debug("Creating second client");
clientDao.createClient(new Client(PERSONAL_NUM));
O resultado da execução acima, juntamente com o log do hibernate, será:
Creating first client
insert into Client (name, personalNumber, id) values
(?, ?, ?)
Changing first client
Deleting first client
Trying to load first client
delete from Client where id=?
select client0_.id as id2_, client0_.name as name2_,
client0_.personalNumber as personal3_2_
from Client client0_ where client0_.personalNumber=?
Creating second client
insert into Client (name, personalNumber, id) values
(?, ?, ?)
Perceba na saída acima que o insert é executado pontualmente, isso porque toda entidade não transiente deve ter um ID. Sendo assim, o hibernate é obrigado a executar o insert para capturar o ID do mesmo. Por outro lado, o delete só é executado depois que tentamos realizar uma busca do cliente, então entraremos naquela regra citada acima: “O flush() é executado antes de uma query ser executada”.
Por outro lado, podemos mudar o comportamento do Hibernate mudando o ID para Sequence, ou seja, o insert não precisará ser feito pontualmente, pois o Hibernate pode consultar apenas a próxima sequence no banco. Então alteramos a propriedade o ID da classe Client para o código da Listagem 4.
Listagem 4. Alterando a propriedade ID da classe Client
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;
Agora, após executamos todos os comandos listados acima, na listagem 3, teremos uma saída diferente, veja:
Creating first client
select next value for hibernate_sequence from
dual_hibernate_sequence
Changing first client
Deleting first client
Trying to load first client
insert into Client (name, personalNumber, id) values
(?, ?, ?)
delete from Client where id=?
select client0_.id as id2_, client0_.name as name2_,
client0_.personalNumber as personal3_2_
from Client client0_ where client0_.personalNumber=?
Creating second client
insert into Client (name, personalNumber, id) values
(?, ?, ?)
O que mudou? O hibernate deixou o insert para depois, mais especificamente para antes de ser executado o select. O insert foi executado antes do delete por uma prioridade de execuções que o hibernate adota, são elas:
- Inserts, na ordem em que foram executadas;
- Updates;
- Deleção de elementos de collections;
- Inserção de elementos de collections;
- Delete, na ordem em que foram executadas.
Ou seja, o Hibernate pode trocar a ordem de execução das suas querys, conforme o mostrado acima, o que em alguns casos pode ocasionar problemas, necessitando assim do flush(). Por conta da ordem mostrada acima é que o insert foi executado logo antes do delete, que foi executado logo antes de query SELECT que disparou o flush() do hibernate.
Vamos supor agora que resolvemos remover a consulta: nesse caso você já deve ter notado que o Hibernate não mais irá realizar o flush(), dado as condições que consideramos logo no início do artigo. Comentamos a seguinte linha:
//LOG.debug("Trying to load first client");
//assertNull(clientDao.
//loadClientWithPersonalNumberOrReturnNull
//(PERSONAL_NUM));
Após executar o mesmo bloco de código você terá uma saída com um erro, como mostrado abaixo:
Creating first client
select
next value for hibernate_sequence from dual_hibernate_sequence
Changing
first client
Deleting
first client
Creating
second client
insert
into Client (name, personalNumber, id) values (?, ?, ?)
insert
into Client (name, personalNumber, id) values (?, ?, ?)
SQL
Error: 0, SQLState: null
failed
batch
Could
not synchronize database state with session
org.hibernate.exception.GenericJDBCException:
Could not execute JDBC batch update
Não iremos mostrar todo erro, pois é desnecessário, o importante é você visualizar que o Hibernate está tentando executar os dois inserts antes do delete (de acordo com a ordem que mostramos). Seguindo essa lógica ele está inserindo um segundo Client com o mesmo personalNumber do primeiro, porém, o personalNumber é uma chave única, causando o erro acima. Caso você remova a constraint unique do personalNumber, verá que o código funciona normalmente. Mas então qual é a solução se quisermos continuar com a constraint? Executar um flush() logo após o delete, assim forçamos a remoção do Client do banco de dados pontualmente.
O write-behind é uma técnica muito importante para otimização de tarefas no hibernate, mas que pode ocasionar sérios problemas quando não usada de maneira correta. O principal objetivo deste artigo foi demonstrar como utilizar o flush() em conjunto com a técnica write-behind, dando exemplos e conceitos importantes para tal.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo