Conheça o Spring Transactional Annotations

Veja neste artigo como funciona em detalhes a anotação @Transactional do Spring Framework.

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:

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?

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:

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:

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:

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.

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.

Artigos relacionados