Trabalhando com Batch Processing de maneira eficaz utilizando a JPA

Frequentemente temos que desenvolver funcionalidades que lidarão com grandes volumes de dados. Imagine por exemplo uma situação onde temos que ler um arquivo em algum formato específico(csv, xml, json, dentre outros), que contêm milhões de registros de vendas, e em seguida devemos inserir todos esses registros em um banco de dados.

Trabalhando com a Java Persistence API, poderíamos ter a seguinte classe representando a entidade Venda:

@Entity
public class Venda {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal valor;
    private String cliente;
    private String vendedor;
    private Calendar data;

    //getters e setters...
}

E o seguinte método para persistir os registros contidos no arquivo:

public void cadastraVendasContidasNoArquivo(InputStream arquivo) {
    EntityManager em = //recupera o EntityManager
    em.getTransaction().begin();

    Venda efetuada = recuperaProximaVenda(arquivo);
    while (efetuada != null) {
        em.persist(efetuada);
        efetuada = recuperaProximaVenda(arquivo);
    }

    em.getTransaction().commit();
    em.close();
}

O código anterior, embora esteja correto, é pouco eficaz e pode causar um OutOfMemoryError.

O problema é que estamos percorrendo a lista de vendas, e, para cada objeto desta lista, invocando o método persist, que faz com que o objeto passe a ser gerenciado pelo contexto de persistência, além de colocá-lo no cache de 1º nível.

Colocar milhões de objetos no cache de 1º nível da JPA quase sempre é inviável. Uma solução mais eficaz seria persistir os objetos aos poucos, fazer a sincronização com o banco de dados, e em seguida já remover esses objetos do cache de 1º nível:

public void cadastraVendasContidasNoArquivo(InputStream arquivo) {
    EntityManager em = //recupera o EntityManager
    em.getTransaction().begin();

    int contador = 1;
    Venda efetuada = recuperaProximaVenda(arquivo);
    while (efetuada != null) {
        em.persist(efetuada);

        //a cada 50 objetos, faz a sincronizacao e limpa o cache
        if (contador % 50 == 0) {
            em.flush();
            em.clear();
        }
        efetuada = recuperaProximaVenda(arquivo);
        contador++;
    }

    em.getTransaction().commit();
    em.close();
}

O código anterior utiliza um contador para controlar quando sincronizar e limpar o cache, que no nosso exemplo será a cada 50 objetos que forem persistidos. Com essa alteração evitamos colocar milhões de objetos no cache de 1º nível.

Outra situação comum, onde temos que lidar com um grande volume de dados, ocorre quando precisamos atualizar uma grande quantidade de registros de uma determinada tabela no banco de dados. Por exemplo, imagine que precisamos criar uma funcionalidade que será responsável por atualizar o status de todos os Contratos que estejam vencidos. Podemos implementar essa funcionalidade com o seguinte código:

public void atualizaOStatusDosContratosVencidos() {
    EntityManager em = //recupera o EntityManager
    em.getTransaction().begin();
    
    String query = "select c from Contrato c where c.dataVencimento < :hoje and c.status = :ativo";
    List<Contrato> vencidos = em.createQuery(query, Contrato.class)
            .setParameter("hoje", Calendar.getInstance())
            .setParameter("ativo", StatusDoContrato.ATIVO)
            .getResultList();
    
    for(Contrato vencido : vencidos) {
        vencido.alteraStatusPara(StatusDoContrato.VENCIDO);
    }
    
    em.getTransaction().commit();
    em.close();
}

Novamente não há nada de errado com o código anterior, ele apenas é ineficaz, pois carrega todos os objetos para a memória e para o cache de 1º nível.

Estamos carregando todos os objetos para a memória apenas para alterar o atributo status, e em seguida esses objetos não são mais utilizados. Nesse caso poderíamos enviar uma instrução SQL de update diretamente para o banco de dados, sem precisar carregar os objetos para a memória:

public void atualizaOStatusDosContratosVencidos() {
    EntityManager em = //recupera o EntityManager
    em.getTransaction().begin();

    String query = "update Contrato c set c.status = :vencido where c.dataVencimento < :hoje and c.status = :ativo";
    em.createQuery(query)
            .setParameter("vencido", StatusDoContrato.VENCIDO)
            .setParameter("hoje", Calendar.getInstance())
            .setParameter("ativo", StatusDoContrato.ATIVO)
            .executeUpdate();

    em.getTransaction().commit();
    em.close();
}

O código foi alterado para utilizar o método executeUpdate(), que executa uma instrução de update ou delete. Com essa pequena alteração evitamos carregar, desnecessariamente, milhões de objetos para a memória, e assim ganhamos em performance e eficácia.

Com essas dicas que vimos conseguimos evitar problemas como OutOfMemoryError e lentidão na execução de rotinas batch.

Você pode conhecer outros problemas comuns que enfrentamos quando utilizamos a JPA no nosso curso de Persistência com JPA, Hibernate e EJB lite, e aprender como resolvê-los de maneira eficaz.

E você, já passou por algum problema ao trabalhar com Batch Processing com a JPA?

21 Comentários

  1. Roberto Shizuo 01/10/2013 at 13:21 #

    Muito bom Rodrigo. O pessoal sempre toma outofmemory quando faz batch com jpa.

  2. Alexandre Aquiles 01/10/2013 at 13:53 #

    Ótimas dicas, Rodrigo!

    Quem nunca enfrentou lentidão com JPA ao manipular um volume razoável de dados?

    Nada como saber como funcionam as ferramentas para evitar esse tipo de problema!

    Essa técnica de flush() / clear() é fundamental. E usar SQL pode ser uma boa opção para alguns casos.

    Para quem usa Hibernate direto, há ainda a StatelessSession.

  3. Rafael Ponte 01/10/2013 at 13:59 #

    Olá,

    Excelente post, dicas super valiosas para quem trabalha com JPA e tem que processar milhares de objetos/entidades.

    No QConSP 2012 eu tive a chance de palestrar sobre “Hibernate Efetivo” (http://www.slideshare.net/rponte/hibernate-efetivo-qconsp2012) e entre as dicas estava o processamento de batch. Se você utiliza Hibernate como provider então é possível tirar proveito do StatelessSession do Hibernate, pois ele meio que cancela o contexto de persistência, evitando assim sobrecarregar a memória.

    Enfim, dá para tirar mais proveito ainda se a aplicação estiver utilizando Hibernate.

  4. Rodrigo Ferreira 01/10/2013 at 14:14 #

    Oi Alexandre e Rafael,

    Verdade, se estivermos utilizando o Hibernate como provider, é uma boa utilizar StatelessSession, evitando assim guardar os objetos no cache de 1º nível.

    E agora com a JPA 2.1 é possível também executar Stored Procedures, com a utilização da interface StoredProcedureQuery.

    Abraços!

  5. Criação 01/10/2013 at 17:12 #

    Conforme disse o Rodrigo o armazenamento em cache é interessante mas aumenta e muito o consumo de CPU.

  6. Raphael Lacerda 01/10/2013 at 17:12 #

    mesmo usando JPA eu prefiro usar a StatelessSession do Hibernate. Só que tem que busca-la a partir da EntityManagerFactory -> HibernateEntityManagerFactory -> SessionFactory -> StatelessSession

    Uma outra dica é acessar a página de customização do trabalho em batch do Hibernate http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html
    e ver outros parametros como hibernate.jdbc.batch_size 20

  7. Hugo Tota 01/10/2013 at 22:04 #

    JPA/Hibernate é simplismente ineficiente, seja qual for a abordagem, para batch com grandes volumes de dados.

  8. Rodrigo Ferreira 02/10/2013 at 12:51 #

    Oi Hugo,

    Conhecendo e utilizando bem os recursos da JPA/Hibernate, como os que foram mostrados nesse post, você consegue ter uma boa eficácia ao trabalhar com batch.

    Comparado com JDBC puro, a diferença de performance é bem pequena, não compensando então devido a complexidade de se trabalhar com JDBC.

    Pode ser viável também trabalhar com a JPA, mas as rotinas batch serem codificadas com JDBC puro 🙂

    Abraços!

  9. Daniel Della Savia 02/10/2013 at 13:16 #

    Olá, Rodrigo! Ótimo post, mas me surgiu uma dúvida: em projetos web, como conciliar essa estratégia com o padrão OSV (Open Session View)? Pois o commit só poderá ser executado no final do procedimento, certo? O que aconteceria se, durante o processamento, fosse lançada uma RunTimeException ou algo do tipo? O que aconteceria com informações já persistidas?

    Abraços!

  10. Rodrigo Ferreira 02/10/2013 at 13:57 #

    Oi Daniel,

    Você pode envolver a rotina batch em uma transação, fazendo o rollback caso ocorra um erro, e assim todas as operações serão desfeitas.

    Geralmente essas rotinas batch são executadas automaticamente pelo sistema, usando alguma API de agendamento como o Quartz ou o TimerService, mas pensando em uma aplicação web usando o conceito do Open Session/EntityManager In View, e com um usuário executando a rotina manualmente, o fluxo é o de uma requisição normal, onde um EntityManager será aberto no inicio da requisição, e o commit(ou rollback caso ocorra algum erro) e fechamento após a view ser renderizada.

    Nesse caso seria interessante disparar a requisição de maneira assíncrona, para evitar que o usuário fique travado por muito tempo, já que essas rotinas em batch demoram bastante tempo.

  11. Gamarra 04/10/2013 at 01:12 #

    Excelente post e excelentes comentários! Já passei por diversas situações onde meter a mão no SQL foi o recurso utilizado. Não preciso nem citar os riscos disso né? O fato é que o post traz algumas boas soluções para reduzir os riscos e melhorar o resultado. Parabéns Rodrigo!

  12. Thiago 04/10/2013 at 11:33 #

    Ótimo post!
    Ontem fiz um método que recebe um arquivo txt pelo navegador e realiza o import do conteúdo no banco.
    Nos meus testes, esse processo estava demorando pouco mais de 2 horas, o que achei estranho e estava até começando a pensar em desistir de utilizar o JPA para este procedimento.
    Seguindo a dica do post e adicionando um flush() e um clear() este processo passou a durar menos de 5 minutos. Uma diferença considerável.
    Sou iniciante em JPA e este post ajudou muito.
    Obrigado, abraço.

  13. Wesley 10/10/2013 at 08:19 #

    Procedimento Simples e inteligente!

  14. Marcos Leon 20/11/2013 at 00:26 #

    Ótimo post!

    Falando sobre isso, para quem utiliza o Java EE 7 vale a pena dar uma olhada no Batch Processing (JSR-352).

    http://docs.oracle.com/javaee/7/tutorial/doc/batch-processing.htm

  15. William 22/11/2013 at 09:07 #

    Conhecendo um pouco de SQL, pode-se melhorar muito mais a performance de uma carga de dados como essa, criando uma stored procedure invocando um comando LOAD DATA INFILE falando do MySQL. outros SGBD´s possuem comando semelhante. E chamando com uma NamedNativeQuery do JPA.

  16. Rodrigo Ferreira 22/11/2013 at 14:03 #

    Oi William, agora com a JPA 2.1 será possível executar stored procedures com o uso da interface StoredProcedreQuery: https://blogs.oracle.com/arungupta/entry/jpa_2_1_early_draft

  17. Wesley Martins 12/09/2014 at 13:58 #

    Seria possível utilizar o procedimento acima dentro de uma Thread?

  18. Rodrigo Ferreira 19/09/2014 at 17:17 #

    Oi @Wesley, você quer dizer colocar o código acima dentro do método run de uma thread?
    Se for isso funciona tranquilo sim.

    Abraços!

  19. Bruno Oliveira de Alcântara 28/10/2016 at 23:06 #

    Ótimo artigo Rodrigo. Pequeno, simples e direto ao assunto. Gostei muito.

  20. Luciano Ortiz 21/12/2016 at 16:59 #

    Excelente post. Bem na hora em que eu estava precisando aplicar boas praticas. Valeu!

Deixe uma resposta