16 maio 2016

Contendo a propagação de sua transação Spring

Neste post falarei sobre uma solução de configuração de transação spring, que utilizei para satisfazer alguns requisitos de uma feature que implementei num projeto que trabalho atualmente. A feature era o seguinte:

1º Ao publicar um conteúdo, este poderia ou não ser clonado ao mesmo tempo;

2º Ao públicar a versão atual, deve ser gerado uma nova versão de rascunho;

3º Os efeitos da publicação não devem ser propagados para o clone, ou seja, se a publicação der errado, ainda assim o contéudo deve ser clonado;

Com o gerenciamento de transações, podemos especificar seu comportamento no âmbito de propagação. Isso significa poder decidir se nosso metodo de negócio vai executar em uma transação lógica ou física. Se será executado no mesmo escopo transacional, ou propagado através de multiplas transações aninhadas. E o que isso significa afinal de contas?  A forma como o resultado de uma transação interna afetará a externa.

 

Então vamos dar uma olhada…

 

Propagtion REQUIRED (default)

A propagação REQUIRED significa que a mesma transação aberta será usada, caso não exista é criada uma nova. Se múltiplos métodos são configurados com REQUIRED, ambos serão atribuídos a transações lógicas distintas, mas irão compartilhar a mesma transação física.

@Service
public class OuterService {
  @Autowired
  private ContentRepository repository;

  @Autowired
  private InnerService innerService;

  @Override
  @Transactional(propagation=Propagation.REQUIRED)
  public void testRequired(Content content) {
    repository.save(content);
     
    try{
      innerService.testRequired();
    } catch(RuntimeException e){
      // Lida com a axceção
    }
  }

}

@Service
public class InnerService {

  @Override
  @Transactional(propagation=Propagation.REQUIRED)
  public void testRequired() {
    throw new RuntimeException("Forçado um Rollback!");
  }

}

Note que a transação interna lança um unchecked exception (por default é a única que força um rollback) é configurada com REQUIRED. O que significa que estão utilizando a mesma transação física, logo a transação externa falhará, ocasionando rollback.

 

Propagation REQUIRES_NEW

O comportamento REQUIRES_NEW significa que uma nova transação física sempre será criada. Fazendo com que o commit ou rollback da transação interna seja independente da transação externa.

@Service
public class InnerService {

  @Override
  @Transactional(propagation=Propagation.REQUIRES_NEW) //Aqui está a diferença
  public void testRequired() {
    throw new RuntimeException("Forçado um Rollback!");
  }

}

Note que o método interno anotado com REQUIRES_NEW lança uma RuntimeException, afetando a transação interna mas não a externa.

Temos outros comportamentos como NESTED, MANDATORY, NEVER, NOT_SUPORTED e  SUPPORTS.

 

Bom, chega de conversa e vamos a solução encontrada!

 

Temos o seguinte método “faz tudo”:

@Service
public class ManagerPublicationService {
  @Autowired
  private ContentRepository repository;

  @Transactional
  public void publish(final Content actualContent, final Boolean clone) {

    //clona o conteúdo atual
    if (clone) {
      Content cloned = actualContent.clonning();
      this.repository.save(cloned);
    }

    //Gera uma nova versão do conteúdo atual
    Content newVersion = actualContent.versioning();
    this.repository.save(newVersion);

    //Aqui vai a regra de negócio para preparar o conteúdo para públicação

    //Publica o conteúdo atual
    this.repository.save(actualContent);
  }

}

Para começar, podemos perceber que este método além de fazer coisas demais, não satisfaz nosso requisito. Pois se algo der errado com a publicação o clone também não vai ocorrer, ambos estão compartilhando a mesma transação. Hora do refactoring!

@Service
public class ManagerPublicationService {
  @Autowired
  private ContentRepository repository;

  @Transactional
  public void publish(final Content actualContent, final Boolean clone) {
  //clona o conteúdo
    if (clone) {
      Content cloned = actualContent.clonning();
      this.repository.save(cloned);
    }
   
    this.versioning(actualContent);
  }


  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void versioning(final Content actualContent) {
    //Gera uma nova versão do conteúdo atual
    Content newVersion = actualContent.versioning();
    this.repository.save(newVersion);

    //Aqui vai a regra de negócio para preparar o conteúdo para públicação
 
    //Publica o conteúdo atual
    this.repository.save(content);
  }

}

Melhoramos um pouco, se a publicação falhar, ainda assim o clone do contéudo não será afetado, mas pode ficar melhor:

public class ManagerPublicationService {
  @Autowired
  private ContentRepository repository;

  @Transactional(propagation = Propagation.REQUIRED)
  public void clone(final Content actualContent) {
    // clona o conteúdo
    Content cloned = actualContent.clonning();
    this.repository.save(cloned);
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void publish(final Content actualContent) {
    this.versioning(actualContent);

   //Regra de negócio omitida para publicar o conteúdo
 
   //Publica o conteúdo atual
   this.repository.save(content);
  }

  @Transactional(propagation = Propagation.REQUIRED)
  public void versioning(final Content actualContent) {
    //Gera uma nova versão do conteúdo atual
    Content newVersion = actualContent.versioning();
    this.repository.save(newVersion);
  }

}

Chegamos a solução final, desta maneira temos métodos com responsabilidade única, semanticamente coeso, e o melhor de tudo é que conseguimos o que queríamos. Pois caso ocorra uma falha ao gerar uma nova versão, a publicação falhará e não afetará o clone.
Apesar da facilidade que @Trasacation nos traz, temos alguns aspectos importantes que impactam em nossa aplicação e devemos levá-los em consideraçao na hora de fazermos uso deste poderoso recurso, como a propagação e o isolamento. Como já dizia o tio Ben: “Grandes poderes trazem grandes responsabilidades”.

CTA-ebook-transformação-digital

Leave a Comment