Neste artigo veremos como o Spring trabalha com transações, através da anotação @Transactional. Veremos características como propagação e isolação além das “armadilhas” que estes recursos podem causar.

Porque usar lógicas transacionais dentro do nosso projeto? Se você já trabalhou com procedimentos transacionais em Sistemas Gerenciadores de Banco de Dados, a ideia é exatamente a mesma, você garante a atomicidade de um procedimento.

Uma transação garante que todo processo deva ser executado com êxito, é “tudo ou nada” (princípio da atomicidade). Quando você realiza algum procedimento bancário transações estão intimamente ligadas a todos os seus passos, garantindo que nenhuma informação seja persistida se todo o processo não tiver 100% de êxito.

Se você utiliza JPA poderá trabalhar com transações de forma programática, pois este não dispõe de nenhuma anotação para tal recursos ser habilitado, precisamos programaticamente dizer que desejamos tornar determinado processo transacional.

Listagem 1. Usando transação com JPA


  UserTransaction utx = entityManager.getTransaction();
   
  try {
      utx.begin();
   
      businessLogic();
         
      utx.commit();
  } catch(Exception ex) {
      utx.rollback();
      throw ex;
  }

Na Listagem 1 usamos o EntityManager para capturar uma instância da transação e poder manipulá-la. O método getTransaction() retorna uma instância de UserTransaction que possui três métodos importantes para nossa listagem:

  • begin(): Inicia uma transação;
  • commit(): Finaliza uma transação;
  • rollback(): Cancela uma transação.

Ao chamar o begin(), uma transação é iniciada e tudo que for feito daqui em diante será considerado transacional, quando o commit() é chamada então as informações são persistidas. Se algum erro ocorrer dentro do businesslogic(), o nosso fluxo vai direto ao bloco catch() onde um rollback() é chamado garantindo que nada será persistido.

Qual problema de trabalhar desta forma?

  • Torna-se muito repetitivo e propenso a diversos erros;
  • Erros nesse nível são muito difícil de depurar e reproduzir;
  • O código tornar-se menos legível devido à complexidade técnica adicionado a ele;
  • Erros neste ponto podem ter um impacto muito alto;
  • Teremos problemas se este método chamar um outro método que já está dentro de uma transação;

Qual a melhor forma de trabalhar com transações?

Uma das boas práticas para trabalhar-se com transações é usando as anotações do Spring Framework, mais especificamente a anotação @Transactional.

É claro que você pode estar utilizando outro Framework para realizar tal tarefa, mas em nosso caso daremos enfoque ao Spring. Toda a Listagem 1 se resume a poucas linhas quando usamos o @Transactional, como mostra a Listagem 2.

Listagem 2. @Transactional


  @Transactional
         public void businessLogic() {
           //lógica necessária aqui
         }

Na Listagem 1 precisamos capturar o entityManager, iniciar uma transação, finalizar a transação, tratar os erros dentro do bloco try-catch e chamar o rollback() caso necessário. Na Listagem 2 só anotamos o método com a anotação @Transactional e pronto, tudo que fizemos na Listagem 1 já está pronto na Listagem 2.

Muito aspectos melhoram quando usamos este recurso, como por exemplo a legibilidade do código, e até a possibilidade de chamar outros métodos que já estejam em transação, “juntando” uma transação a outra, veremos isso a mais à frente.

@Transactional

Algo muito importante a ser entendido são dois conceitos chaves:

  • PersistenceContext;
  • Transação do Banco de Dados.

A anotação @Transactional trabalha dentro do escopo de uma transação no banco de dados, a transação do banco de dados ocorre dentro do PersistenceContext, que por sua vez, está dentro do EntityManager que é implementado usando Hibernate Session (quando você está usando o Hibernate como container).

O PersistenceContext funciona como um container que armazena os objetos em memória até que eles sejam sincronizados com o banco de dados.

Veremos agora alguns atributos importantes que podem ser utilizados dentro da anotação @Transactional.

Propagation

Este atributo definir o nível de propagação de determinada transação, que pode ser:

  • PROPAGATION_REQUIRED: Este definir que obrigatoriamente aquele bloco de código deve ser executado dentro de uma transação. Se uma transação já existir então ele usará esta existente, caso contrário ele criará uma nova. Este é o padrão quando não definido nenhum nível de propagação explicitamente.
  • PROPAGATION_SUPPORTS: Se uma transação já existir então ela será utilizada caso contrário nenhuma será criada.
  • PROPAGATION_MANDATORY: Caso exista uma transação ela será utilizada, caso contrário uma exceção será lançada, TransactionRequiredException, pois este tipo de propagação requer que uma transação já esteja criada previamente.
  • PROPAGATION_REQUIRED_NEW: Sempre cria uma nova transação. Se nenhuma transação existir então ela será criada, porém se uma já existir ela será suspensa e uma nova será criada. Quando esta transação é finalizada então a transação original é recuperada.
  • PROPAGATION_NOT_SUPPORTED: Uma transação não será necessária no bloco de código anotado com este atributo. Se alguma transação existir então ela será suspensa até o fim do bloco e só depois será retomada.
  • PROPAGATION_NEVER: Muito semelhante ao PROPAGATION_NOT_SUPPORTED, não permitindo o uso de transações no bloco de código, com a única diferença que caso uma transação exista uma exceção será lançada.
  • PROPAGATION_NESTED: O bloco de código será executado em uma transação aninhada se uma outra transação já existir, caso contrário será criada uma nova transação.

Listagem 3. Usando o atributo propagation


  @Transactional(propagation = Propagation.MANDATORY)
  @Transactional(propagation = Propagation.NESTED)
  @Transactional(propagation = Propagation.NEVER)
  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  @Transactional(propagation = Propagation.REQUIRED)
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @Transactional(propagation = Propagation.SUPPORTS)
  

Na Listagem 3 o atributo propagation recebe uma das sete constantes declaradas na classe Propagation.

Read-Only

Uma outra propriedade muito importante no uso de transações é o read-only, identificando que determinada transação não pode realizar operações de escrita ou alterações, apenas leitura, como mostra o código a seguir:

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)

Isolation

Esta propriedade define qual nível de isolação da transação corrente, sendo que o mesmo utiliza o padrão do banco de dados quando não é definido nenhum nível de isolação. Vejamos os possíveis níveis:

  1. Default: Usa o padrão do banco de dados;
  2. Read Uncommited: Este é o nível menos isolado e o como o próprio nome já sugere, ele permite a leitura antes da confirmação. Neste nível de isolação todos os problemas (Dirty Reads, Nonrepeatable reads e Phatom Reads) podem ocorrer sem restrição. É muito difícil que esse nível seja aplicado na prática pois poderiamos ter sérios problemas de consistência, por isso ele é considerado mais acadêmico, apenas para fins de estudos;
  3. Read Commited: Neste nível de isolação não podem ocorrer Dirty Reads mas são permitidos Nonrepeatable reads e Phantom Reads;
  4. Repeatable Readed: Aqui apenas ocorrem Phantom Reads. O SGBD bloqueia o conjunto de dados lidos de uma transação, não permitindo leitura de dados alterados ou deletados mesmo que comitados pela transação concorrente, porém ele permite a leitura de novos registros comitados por outras transações;
  5. Serializable: Este é o nível mais isolado que não permite nenhum tipo de problema (Dirty Read, Nonrepeatable read e Phantom Read);
A ideia para usar o nível de isolação é muito parecida com a propagação da transação, você deve usar as constantes presentes na classe Isolation, como mostra a Listagem 4.

Listagem 4. Usando isolation


  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS, isolation = Isolation.DEFAULT)
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS, isolation = Isolation.READ_COMMITTED)
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS, isolation = Isolation.READ_UNCOMMITTED)
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS, isolation = Isolation.REPEATABLE_READ)
  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS, isolation = Isolation.SERIALIZABLE)

Timeout

Define o tempo limite para a transação ser abortada automaticamente, por padrão é utilizado o timeout do banco de dados corrente.

Aplicação Prática

Dada as informações acima sobre o uso de transações com Spring, vejamos nesta seção como aplicar na prática o uso destes recursos.

Nossa aplicação consistirá em criar métodos que utilizam a anotação @Transactional da forma necessária, umas com readOnly=true outras com readOnly=false, algumas com Propgation A e outras com propagation B.

Nosso primeiro exemplo consistirá em um método para buscar determinada entidade através do seu ID, como mostra aListagem 5.

Listagem 5. Buscando Entity através do ID


  @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
  public Object findById(Class clazz, Integer id) {
    try {
             logger.info("Procurando pelo Bean " + clazz.getName() + " com ID "
                   + id);
             Query query = entityManager.createQuery("SELECT c FROM " + clazz.getName()
                   + " c WHERE c.id = :id");
             query.setParameter("id", id);
             Object resultBean = (Object) query.getSingleResult();
   
             if (resultBean != null)
               logger.info("Objetos Encontrados: 1");
             else
               logger.info("Objetos Encontrados: 0");
   
             return resultBean;
         } catch (Exception e) {
             e.printStackTrace();
             logger.error("Ocorreu um erro ao executar o findById. MSG ORIGINAL: "
                   + e.getMessage());
         }
    }

O nosso método da Listagem 6 recebe como parâmetro uma Classe e um ID do tipo inteiro. Perceba que a única função do método é buscar este objeto no banco de dados através do seu id, então logo percebemos que não precisaremos fazer nenhuma alteração no banco, apenas consulta, sendo assim colocamos a propriedade readOnly=true para garantir que nada seja alterado dentro deste método.

Naturalmente os métodos que azem consulta ao banco são usados dentro de alguma lógica de negócio, correto? Situação hipotética: Você irá inserir um novo cliente, mas precisa checar antes se este cliente já foi inserido no banco. Sendo assim, o ideal é que os métodos de consulta utilizem a transação corrente, ou seja, una-se a esta transação.

Se por acaso o método findById() for executado diretamente, sem estar dentro de nenhuma lógica então não precisaremos de transação alguma visto que é apenas uma consulta que não alterará nada no banco, sendo assim usamos o Propagation.SUPPORTS. Reforçando a ideia: Quando a consulta for realizada dentro de alguma lógica do sistema e esta possuir uma transação ela irá unir-se a esta transação, caso contrário nenhuma transação será necessária para uma busca deste tipo.

Vamos entender o passo a passo do método acima:

  • A variável logger é utilizada pela Framework Log4j para armazenar logs sobre o que está acontecendo dentro do método, mas você pode alterar para algo mais simples como um System.out.println().
  • Criamos uma query através do createQuery que tem como filtro o id:
    Query query = entityManager.createQuery("SELECT c FROM " + clazz.getName()
                     + " c WHERE c.id = :id");
           query.setParameter("id", id);
  • Usamos o getSingleResult() para retornar o único objeto presente da resposta do banco de dados:
              Object resultBean = (Object) query.getSingleResult();
     
               if (resultBean != null)
                 logger.info("Objetos Encontrados: 1");
               else
                 logger.info("Objetos Encontrados: 0");

    Aproveitamos para checar se o “resultBean” não é nulo, caso contrário mostraremos uma mensagem no log dizendo que 0 objetos foram encontrados.
  • O retorno sempre será o resultBean independente se ele é ou não nulo.

No exemplo acima estamos usando uma busca que não depende de nenhuma transação, apenas usando-a caso exista. Agora veremos um exemplo na Listagem 6, onde a transação é requerida.

Listagem 6. O método save()


  @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
      public Object save(Object bean, boolean flushAndRefresh) {
         try {
             logger.info("Salvando Bean " + bean.getClass().getName());
             bean = entityManager.merge(bean);
   
             if (flushAndRefresh) {
               entityManager.flush();
               entityManager.refresh(bean);
             }
   
             return bean;
   
         } catch (Exception e) {
             e.printStackTrace();
             logger.error("Ocorreu um erro ao tentar salvar. MSG ORIGINAL: "
                   + e.getMessage());
         }
      }

O método save(), como o próprio nome já sugere, realiza o merge (inserção ou atualização) do nosso Bean na base de dados. Logo de início podemos perceber que a propriedade readOnly foi configurada para “false” diferente da listagem 6 que estava “true”. Isso ocorreu pois neste caso precisamos alterar as informações no banco de dados e não apenas consultar.

Na próxima propriedade temos o Propagation.REQUIRED exige que o método save() seja executado dentro de uma transação, caso essa transação já exista ele apenas a utiliza caso contrário ele cria uma nova.

É importante que o save() esteja dentro de uma transação para garantir que os dados só serão salvos fisicamente no banco de dados caso todos os processos internos ao método sejam executados com sucesso. Em nosso caso estamos executando um simples merge() mas pense que o método save() pode estar cascateado com outros saves, como no exemplo da Listagem 7.

Listagem 7. Save em cascata


  for(Object o : objects){
               save(o);
  }

Na Listagem 8 quando o primeiro save() for chamado então uma transação será criada, porém no segundo save() ele irá utilizar a transação já existente e assim por diante para os próximos saves. Quando ele terminar a iteração então todos os objetos serão persistidos.

Listagem 8. Simulando erro na iteração


  int i = 1;
  for(Object o : objects){
         if (i == 10){
               throw new RuntimeException("Forçando rollback da transação");
         }
         save(o);
         i++;
  }

Na Listagem 8 demonstramos que nove saves são executados com sucesso mas ao chegar no save número 10 uma exceção é lançada e todos os nove saves são abortados, ou seja, um rollback é feito para toda a transação. Lembre-se que todos esses saves estão na mesma transação.

Vamos agora entender o funcionamento interno do método save() demonstrado acima, passo a passo.

  • Usamos o mesmo logger já explicado na listagem anterior, para gravar informações sobre o processo.
  • Chamamos o método merge() que irá se responsabilizar de inserir ou atualizar o bean no banco de dados, retornando o bean atualização ou inserido:
     bean = entityManager.merge(bean);
  • O atributo booleando flushAndRefresh serve para realizar um flush() e um refresh() no PersistenceContext, quando necessário. Assim os dados que foram salvos que antes estavam em memória são imediatamente sincronizados com a base de dados, forçando a sua atualização imediata:
    if (flushAndRefresh) {
           entityManager.flush();
           entityManager.refresh(bean);
    }
  • Por fim o retorno sempre será o atributo bean que foi atualizado na base.

Juntando as Listagens 5 e 6 mostradas como exemplo para entendimento do @Transactional você poderá perceber que as duas juntas fazem exatamente o que deveriam com o Propagation.SUPPORTS e o Propagation.REQUIRED. Veja bem, se precisássemos buscar alguma informação dentro do save() através do id poderíamos usar o findById() já criado que ele utilizaria a mesma transação do save(). Vejamos um exemplo disto na Listagem 9.

Listagem 9. Listagem Unindo o save() com o findById()


  @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
      public Object save(Object bean, boolean flushAndRefresh) {
         try {
             logger.info("Salvando Bean " + bean.getClass().getName());
             
             Object result = findById(bean.getClass(), 10);
             if (result != null){
                throw new RuntimeException("O ID 10 é reservado para futuras implementações. Operação abortada");
             }      
             
             bean = entityManager.merge(bean);
   
             if (flushAndRefresh) {
               entityManager.flush();
               entityManager.refresh(bean);
             }
   
             return bean;
   
         } catch (Exception e) {
             e.printStackTrace();
             logger.error("Ocorreu um erro ao tentar salvar. MSG ORIGINAL: "
                   + e.getMessage());
         }
      }

Veja que adicionamos a seguinte linha:

Object result = findById(bean.getClass(), 10);
  if (result != null){
         throw new RuntimeException("O ID 10 é reservado para futuras implementações. Operação abortada");
  }

Neste momento temos uma transação em execução já que o save usa o Propagation.REQUIRED, quando entrarmos no findById() ele apenas usará a transação já existente. O retorno deverá ser nulo para que o processo continue, caso contrário uma exceção será lançada e automaticamente um rollback será feito.

Neste artigo vimos como utilizar em detalhes a anotação @Transactional do framework Spring. Trata-se de uma anotação especifica para trabalhar com transações, seguindo a mesma ideia de transação no banco de dados, utilizando também seus conceitos base como: nível de isolação, propagação e permissão de escrita e leitura (ReadOnly).

É importante entender o funcionamento destes conceitos para trabalhar de forma correta com frameworks ORM que gerenciam a persistência ao banco de dados. Neste ponto o Spring trabalha como um recurso extra para frameworks como o JPA, que realiza a gerência dos dados que serão persistidos efetivamente.

Para treinar o funcionamento de tais recursos o ideal é que você crie um projeto utilizando os dois métodos básicos apresentados nas Listagens 5 e 6, onde você poderá verificar o que ocorre ao mudar os níveis de propagação de cada um, trabalhando com eles em conjunto e de forma independente.