O Hibernate é um poderoso framework que facilita a maioria do serviços desenvolvidos que são relacionados a banco de dados, no entanto, devemos entender algumas estratégias que o Hibernate executa durante seus procedimentos, principalmente quando vamos trabalhar com lote de dados, pois ele pode facilitar bastante o processo mas pode comprometer a performance se não entendermos corretamente o que será executando durante a execução de uma grande quantia de dados subsequentes.
Cache de primeiro nível
A principal funcionalidade que devemos entender é o cache de primeiro nível do Hibernate porque todo objeto que será persistido é adicionado no cache de primeiro nível até que a sessão seja liberada, ou seja, caso tiver 1000 registros para serem processados os 1000 objetos serão alocados em memória até o commit da transação onde será limpado a sessão.
Esse é o comportamento padrão do Hibernate, podemos ver que essa estratégia irá funcionar independente do cenário e não terá problemas com processamento com poucos registros, no entanto, é visível que esse cenário pode se agravar conforme o aumento de registros processados.
Limpando a sessão manualmente
A forma de minimizar essa degradação na performance é limpando a sessão conforme uma quantia de dados processados, que vai variar de acordo com o cenário do projeto.
A limpeza da sessão é feita através do método clear do entityManager (ou da session utilizada no momento), mas antes de limpar esses dados do cache precisamos executar o flush para sincronizar esses dados para que não sejam perdidos.
Exemplo:
@Transactional public void processar() { final List<Entidade> entidades = servico.buscarEntidadesParaAtualizar(); final int TAMANHO_LOTE = 30; int i = 0; for(Entidade entidade : entidades) { //executa alguma alteração na entidade entidade.processar(); entityManager.merge(entidade); if ( i % TAMANHO_LOTE == 0 ) { entityManager.flush(); entityManager.clear(); } }
- TAMANHO_LOTE: Pode ser de 10 a 50, vai depender de quanto de memória o processamento pode consumir da aplicação.
- entityManager.merge: Realiza o update na base de dados, mas só será executado de fato na chamada do flush.
- entityManager.flush(): Sincroniza o contexto de persistência com a base de dados, ou seja, executa os scripts na dentro na transação.
- entityManager.clear(): Limpa o contexto de persistência, inclusive o cache de primeiro nível e os scripts SQLs que estavam em memória aguardando o final da execução.
Configurando batch_size do Hibernate
Outra solução para melhorar a performance é configurar o batch size do Hibernate, que é uma propriedade que ativa a inserção/atualização em lote no hibernate e determina o número de inserções/atualizações que serão executada em cada lote, isso é necessário para evitar OutOfMemoryException durante a execução do processamento, pois além do cache de primeiro nível o Hibernate também insere todas entidades em um cache a nível de sessão e configurando essa propriedade ele limpará esse dados de acordo com o tamanho do size definido na configuração.
persistence.xml
... <properties> <property name="hibernate.jdbc.batch_size" value="30" /> </properties>
A documentação do hibernate recomenda o tamanho de 10-50.
Obs: É uma recomendação da documentação do Hibernate utilizar o mesmo valor definido no batch_size nas operações em batch que utilizam flush/clear.
Performance em processamentos em lote…
Também há outras formas de melhorar a performance dos processamentos em lotes, mas aí já estão relacionadas a diferentes implementações como: paginar o processamento,refinar as consultas que antecedem o processamento, trabalhar com VO(Value Object) ao invés de entidades ou até mesmo chamar SQL nativos em casos específicos.