Erros que você provavelmente comete: veja as melhores práticas para controle de transações

Por dti digital|
Atualizado: Mar 2018 |
Publicado: Ago 2015

Em um sistema moderno, é comum que tenhamos interações entre diferentes fontes de dados que devem ser coordenadas para produzir resultados atômicos. Normalmente, tais fontes de dados são sistemas de bancos de dados relacionais e a coordenação de resultados entre elas é realizada através de transações.

Mas qual a melhor forma de garantir que estes resultados sejam mesmo atômicos? Como fazer com que problemas em uma das fontes de dados façam com que as operações realizadas em outras fontes também sejam desfeitas? Como evitar com que a transação criada seja acoplada entre camadas da aplicação? É possível transacionar outras fontes de dados que não sejam bancos de dados?

Estas perguntas são todas respondidas com a utilização das entidades contidas no pacote System.Transactions, parte integrante do .NET Framework desde sua versão 2.0, principalmente sua classe TransactionScope.

É normal, em sistemas pequenos, com uma única fonte de dados e pouquíssimas interações em cada operação de negócio, que uma fonte de dados de sistemas de banco de dados (daqui para frente referenciada pela sua interface, IDbConnection), simplesmente crie uma nova transação ao chamar a operação IDbConnection.BeginTransaction(). Esta operação gera uma IDbTransaction e garante que as operações realizadas no banco serão completadas ao executar a operação IDbTransaction.Commit(). Esta abordagem requer que, para uma determinada operação de negócio, a instância de IDbConnection e de IDbTransaction sejam únicas no contexto de execução desta operação, garantindo que todos os acessos a banco passem por estas mesmas instâncias. Somente assim esse método de negócio será realmente transacional.

E aí? Resolvido?

Quase. O problema desta solução é que, se há operações sobre fontes de dados sendo executadas em várias classes dentro do contexto da operação de negócio, as instâncias da conexão e da transação normalmente devem ser passadas como parâmetro para todas estas classes, causando um dos maiores pecados que podem ser cometidos na programação orientada a objetos: o acoplamento. Isso faz com que a assinatura de todas as classes que participam dessa operação de negócio “exponham” parte de sua implementação, tornando difícil o reuso e a manutenção da mesma, assim como uma possível substituição da camada de acesso a dados.

Mas calma, nada é tão ruim que não possa piorar: imagine que sua aplicação possua duas (ou mais) fontes de dados. É fácil perceber como o acoplamento aumenta de forma drástica (duplicando o número de instâncias de IDbConnection e de IDbTransaction para cada fonte de dados adicional), além de causar outro problema ainda mais sério: cada fonte de dados tem sua transação específica, o que quer dizer que, se a primeira for executada com sucesso e a segunda falhar, apenas a segunda será desfeita, perdendo o conceito de operação atômica.

E agora?

Uma melhoria na implementação de transações acima seria a utilização de um Contexto de execução. Ao iniciar a execução de uma operação de negócio, um Contexto estático (por Thread de execução) é criado. Assim, sempre que uma classe necessitar de acesso a uma instância de IDbConnection, a responsabilidade de obtenção da IDbConnection desejada deve ser delegada ao Contexto associado à Thread atual. O Contexto deve ser responsável por manter uma lista atualizada das conexões em uso e criar uma nova sempre que for a primeira requisição pela conexão desejada. Da mesma forma, se uma conexão deve ser transacionada, então o Contexto deve ser responsável por iniciar a transação e sempre utilizar essa mesma transação dentro da operação de negócio. Assim, temos um repositório central que cuida da criação/manutenção das entidades necessárias.

Agora sim acoplamento resolvido, correto?

Na verdade, ainda não. Nós apenas centralizamos os pontos que fazem acesso direto às interfaces de IDbConnection e IDbTransaction, mas ainda há uma dependência forte das classes para o próprio Contexto. Outro problema desta abordagem é que a complexidade de se manter uma lista válida/atualizada de IDbConnection e IDbTransaction se torna responsabilidade do Contexto, dificultando a capacidade de manutenção do código. Sem falar que o problema de duas fontes de dados em uma mesma operação de negócio não transacionais entre si ainda persiste.

É aí que entra a classe TransactionScope.  Ao criar uma instância dessa classe (normalmente utilizando as boas práticas da construção “using”), é garantido que todo o código dentro deste bloco estará contido em uma transação. Obteve uma nova instância de IDbConnection e não chamou BeginTransaction? Sem problema. As implementações mais atuais da maior parte dos drivers de acesso a banco de dados ADO.NET já é integrada à solução do TransactionScope por “debaixo dos panos”: o que a TransactionScope faz é disponibilizar uma instância de uma transação (não mais uma IDbTransaction, mas sim uma ITransaction) na Thread corrente em um local conhecido e as implementações dos drivers (ou qualquer Resource Manager, que é a forma como a MSDN se refere a fontes de dados que suportam transações – não somente fontes de dados de sistemas de banco de dados) conseguem “ver” esta transação e se registrar (ou enlist, no original em inglês) nela.

Parece familiar?

É basicamente a solução de Contexto acima, com a primeira vantagem sendo que a responsabilidade de manutenção das conexões/transações foi delegada para os resource managers/TransactionScope, facilitando a vida do desenvolvedor. Outra vantagem: através de um mecanismo conhecido como Coordenador de Transação Distribuída (DTC, no original em inglês), é possível que duas fontes de dados diferentes compartilhem da mesma transação, fazendo com que ambas se tornem verdadeiramente transacionais entre si. Obviamente, a participação em uma transação DTC é bem mais custosa, mas o pacote System.Transactions é inteligente o bastante para somente promover uma transação para DTC caso necessário. Para participar em uma transação DTC, é necessário que o Resource Manager da fonte de dados suporte a participação nesse tipo de transação.

Bacana, não é? Segue um exemplo bem básico (que desconsidera separações em camadas e outras boas práticas) em pseudo-código:

public void OperacaoDeNegocio(parâmetros)

{

using(TransactionScope escopo = new TransactionScope())

{

ExecutarPrimeiraOperacao();

ExecutarSegundaOperacao();

 

// Se não houve erro em nenhuma das duas operações, marca a operação como concluída. É aqui que acontece o Commit da transação.

escopo.Complete();

}

}

 

private void ExecutarPrimeiraOperacao()

{

using(IDbConnection conn = new SqlServerConnection(“ConexaoSQLServer”))

{

IDbCommand comando = conn.CreateCommand();

comando.Execute(“INSERT INTO usuario VALUES (‘Eduardo Lima’)”);

}

}

 

private void ExecutarSegundaOperacao ()

{

using(IDbConnection conn = new AseConnection(“ConexaoSybase”))

{

IDbCommand comando = conn.CreateCommand();

comando.Execute(“UPDATE execucoes SET numeroExecucoes = numeroExecucoes + 1 WHERE idExecucao = 1”);

}

}

Veja que, acima, não foi preciso explicitamente criar transações nem ficar definindo e reaproveitando conexões (trabalhos estes de responsabilidade da TransactionScope e de cada conexão). Dessa forma, ambas as fontes se tornam transacionais entre si, com um nível de acoplamento baixo. Com uma boa implementação da obtenção da fonte de dados, é possível reduzir o acomplamento a um nível mínimo, facilitando a troca da fonte de dados (por exemplo, trocando o banco de dados Sybase por um Oracle).

O pacote System.Transactions possui várias outras vantagens, como permitir que sejam implementados Resource Managers através da implementação de interfaces do próprio pacote, ou permitir que serviços WCF sejam marcados como transacionais através de alguns atributos. Obviamente há vários outros aspectos relacionados ao uso do pacote System.Transactions e suas vantagens, que não cabem nesse post. Um grande ponto de partida é o artigo do MSDN https://msdn.microsoft.com/en-us/library/ms973865.aspx, leitura obrigatória para quem deseja se aprofundar no assunto.

Vejam que, de forma simples, utilizando apenas recursos nativos do framework .NET, é possível implementar um controle transacional robusto e escalável, refletindo, assim, um dos grandes valores da DTI: o de sempre desenvolver soluções aderentes aos principais padrões de mercado, sem a necessidade de “reinventar a roda”, priorizando assim a agilidade e  mantendo a qualidade das entregas para o cliente.

Por: Eduardo Lima e Jéssica Saliba.

Quer saber mais?