Controle Transacional Programático em Sistemas Legados
Aprenda a gerenciar as transações na sua aplicação Java de forma correta e eficiente
Não importa quão novo na área você seja, cedo ou tarde você cairá de paraquedas em um sistema legado. Um sistema na qual não há qualquer linha de testes, com tecnologias de certa forma defasadas e esquecidas pelo tempo e principalmente onde a maioria dos desenvolvedores não querem colocar as mãos. Esses sistemas, algumas vezes levados com a barriga, costumam apresentar diversos problemas com relação a qualidade de código, como classes com milhares de linhas, muito hard-code e números mágicos, código duplicado e toneladas de comentários desatualizados; mas sem dúvida um dos problemas mais comuns que eu tenho visto neste aspecto está relacionado a camada de persistência, mas especificamente ao controle transacional.
Digo isso, pois não é incomum encontrar código com diversos tipos de problemas, como vazamento de conexão, SQL Injection, transações não comitadas ou desfeitas (rollback), erros engolidos e não logados, processos de negócio que deveriam ser atômicos mas não o são, entre outros. Para entender o que estou falando, vamos imaginar um procedimento onde devemos substituir um livro na prateleira, que basicamente significa remover o livro antigo e inserir o livro novo no banco de dados. Dessa forma, o código seria muito parecido com este:
public class LivroService {
/**
* Substitui um livro no sistema
*/
public void substituir(Livro antigo, Livro novo) {
try {
Connection conexao = new ConnectionFactory().getConnection(); // abre conexão
// remove livro antigo
PreparedStatement stmt1 = conexao.prepareStatement("delete from Livro where id = ?");
stmt1.setInt(1, antigo.getId());
stmt1.execute();
// insere livro novo
PreparedStatement stmt2 = conexao.prepareStatement("insert into Livro(titulo) values(?)");
stmt2.setString(1, novo.getTitulo());
stmt2.execute();
conexao.commit(); // comita transação
} catch (SQLException e) {
e.printStackTrace(); // engole erro
}
}
}
Apesar da funcionalidade ser simples, afinal trata-se de um DELETE
e um INSERT
, esse código possui diversos problemas graves que podem levar desde a inconsistência dos dados até indisponilizar a aplicação ou banco de dados. Para citar alguns deles:
- ausência de controle transacional: não está claro onde uma transação começa e termina, pior, não há rollback da mesma;
- inconsistência de dados: por padrão uma conexão JDBC é criada em modo auto-commit, ou seja, cada comando SQL enviado pro banco é comitado imediatamente após sua execução (se o segundo SQL quebra nós temos um problema grande nas mãos);
- recursos não fechados apropriadamente: os recursos básicos da conexão, como as
PreparedStatement
, não são fechados; - tratamento de erro: exceção é engolida dentro do bloco
try-catch
; - vazamento de conexão: provavelmente o mais grave de todos, a conexão nunca é fechada;
Esse tipo de código normalmente é escrito por desenvolvedores iniciantes que não entendem muito bem como um banco de dados relacional trabalha e tende a se espalhar por todo o sistema - sabe como é, aquele velho Ctrl+C e Ctrl+V em um código problemático mas que ainda funciona. Para consertá-lo, precisaríamos rescrevê-lo como abaixo:
public void substituir(Livro antigo, Livro novo) {
Connection conexao = null;
try {
conexao = new ConnectionFactory().getConnection(); // abre conexão
conexao.setAutoCommit(false); // inicia a transação
// remove livro antigo
PreparedStatement stmt1 = conexao.prepareStatement("delete from Livro where id = ?");
stmt1.setInt(1, antigo.getId());
stmt1.execute();
// insere livro novo
PreparedStatement stmt2 = conexao.prepareStatement("insert into Livro(titulo) values(?)");
stmt2.setString(1, novo.getTitulo());
stmt2.execute();
conexao.commit(); // comita transação
} catch (SQLException e) {
if (conexao != null) {
try {
conexao.rollback(); // desfaz alterações enviadas pro banco
} catch (SQLException e1) {
e1.printStackTrace();
}
}
throw new RuntimeException(e); // relança exceção
} finally {
if (conexao != null) {
try { conexao.close(); } catch (SQLException e) {} // fecha conexão e todos seus recursos
}
}
}
Repare que embora os problemas tenham sido corrigidos, o desenvolvedor precisa escrever o dobro de linhas, que por sinal, não são linhas de lógicas de negócio, mas linhas de código de infraestrutura. Não é por acaso que é considerado infraestrutura, o cerne de todo o código acima está em como delimitamos onde uma transação inicia e termina, e o que fazemos em caso de erro ou sucesso da sua execução, por esse motivo chamamos essa estrutura (layout) de controle transacional.
Não se engane, este controle das transações deve ser feito para todos os comandos SQL enviados pro banco, mesmo um simples SELECT
precisa estar sob este guarda-chuva. Mas como você já deve ter percebido, ter todo esse código de infraestrutura espalhado pela aplicação não é saudável, pois mata a legibilidade do código de negócio, além de ser muito propício a erros - ora ou outra alguém esquece de comitar ou fechar uma conexão. Para evitar estes deslizes, e levando em consideração que esta é uma estrutura padrão para qualquer comando SQL, nós podemos tratá-lo como um template, que por sinal pode ser resumido no trecho abaixo:
Connection connection = null;
try {
connection = // abre uma nova conexão
connection.setAutoCommit(false); // inicia a transação
// --
// sua lógica de negócio vai aqui
// --
connection.commit(); // comita transação
} catch (Exception e) {
connection.rollback(); // desfaz alterações enviadas pro banco
throw new RuntimeException(e); // relança exceção
} finally {
connection.close(); // fecha conexão e todos seus recursos
}
Esse template será utilizado para qualquer comando SQL que executarmos, dessa forma evitamos todos os possíveis problemas que discutimos anteriormente.
A pergunta que fica agora é: como transformar esse template num código reutilizável?
Controle transacional programático
A idéia agora é criar uma API simples para o controle transacional e, aos poucos, substituir todos os trechos de código que lidam com persistência.
Antes de tudo, entenda uma transação como uma unidade de trabalho lógica e atômica, onde esta unidade pode conter um ou mais comandos SQL. Por ser atômica, todos esses comandos devem terminar juntos, seja com sucesso ou erro. Resumindo, ou vai tudo pro banco ou não vai nada!
Desse modo, vamos usar um pouco de orientação de objetos para criar uma classe especializada em gerenciar as transações, chamaremos ela de TransactionManager
, ela terá um único método para encapsular o template acima e deve receber uma lógica via parâmetro. Para este método receber uma lógica qualquer via parâmetro, vamos usar o padrão de projeto Strategy, ou seja, o parâmetro será uma abstração (interface Java) da nossa unidade de trabalho:
public class TransactionManager {
private ConnectionFactory factory = new ConnectionFactory();
/**
* Executa uma lógica de negócio (callback) dentro de um contexto transacional
*/
public void doInTransaction(TransactionCallback callback) {
Connection conexao = null;
try {
conexao = this.factory.getConnection(); // abre conexão
conexao.setAutoCommit(false); // inicia a transação
// --
callback.execute(conexao); // sua lógica executada aqui
// --
conexao.commit(); // comita transação
} catch (Exception e) {
if (conexao != null) {
// desfaz alterações enviadas pro banco
try { conexao.rollback(); } catch (SQLException e1) { e1.printStackTrace(); }
}
throw new RuntimeException(e); // relança exceção
} finally {
if (conexao != null) {
// fecha conexão e todos seus recursos
try { conexao.close(); } catch (SQLException e) {}
}
}
}
}
E nossa lógica (unidade de trabalho) seria representada pela interface TransactionCallback
:
public interface TransactionCallback {
public void execute(Connection connection) throws SQLException;
}
Repare que a idéia é o callback ser executado dentro de um contexto transacional, por esse motivo ela recebe a conexão aberta por nosso gerenciador.
Excelente! Para usar nosso gerenciador bastaria um código como este:
TransactionManager txManager = new TransactionManager();
txManager.doInTransaction(new TransactionCallback() {
@Override
public void execute(Connection connection) throws SQLException {
// sua lógica de negócio vai aqui
}
});
Perceba que passamos uma classe anônima pra chamada do método doInTransaction()
, já que se trata de uma lógica dinâmica no sistema. Usar classes anônimas nos permite passar um bloco de código (função) como referência em Java, algo próximo do que conseguimos com linguagens que possuem caracteristicas funcionais, como Ruby, JavaScript e Scala. Lembre-se, estamos num sistema legado, Java 8 e lambda são modinhas.
Agora, com nosso gerenciador de transações pronto, podemos refatorar o código da classe LivroService
para usá-lo:
public void substituir(Livro antigo, Livro novo) {
TransactionManager txManager = new TransactionManager();
txManager.doInTransaction(new TransactionCallback() {
@Override
public void execute(Connection connection) throws SQLException {
// remove livro antigo
PreparedStatement stmt1 = connection.prepareStatement("delete from Livro where id = ?");
stmt1.setInt(1, antigo.getId());
stmt1.execute();
// insere livro novo
PreparedStatement stmt2 = connection.prepareStatement("insert into Livro(titulo) values(?)");
stmt2.setString(1, novo.getTitulo());
stmt2.execute();
}
});
}
Nosso código ficou muito mais simples e com maior clareza, pois desta vez o desenvolvedor se preocupa apenas com a lógica de negócio em si, já que toda a infraestrutura está sob a responsabilidade da classe TransactionManager
; ou seja, sem mais vazamento de conexões nem esquecimento de comitar ou defazer uma transação.
Uma grande vantagem dessa abordagem programática é que a classe TransactionManager
se torna um único ponto de manutenção no sistema, o que nos permite evoluir o controle transacional sem quebrar as classes que a utilizam. Pense bem, podemos futuramente utilizar um pool de conexões alterando somente esta classe.
Ter uma classe responsável pelo controle transacional é uma boa prática, em especial para sistemas legados, onde não existe a figura de um servidor de aplicação Java EE ou container IoC/DI como o Spring.
Leve a idéia para seu projeto que usa JPA e Hibernate
A solução que discutimos aqui funciona muito bem para sistemas que trabalham com JDBC puro, mas ela pode ser levada para sistemas que trabalham com JPA ou Hibernate - eu até dei uma brincada com Java 8 e generics na tentativa de implementar uma API genérica. Por exemplo, para o controle transacional com JPA teríamos um código muito parecido com o do JDBC:
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.doInTransaction(new JpaTransactionCallback() {
@Override
public void execute(EntityManager em) {
// sua lógica de negócio vai aqui
}
});
Com estes frameworks a solução fica até mais fácil de implementar, já que eles tem uma API de transações bem definida. Se a solução programática não te agrada, não seria muito difícil implementar o controle transacional via Reflections e Proxies - esta com certeza seria uma solução mais elegante.
Trabalhando com Java 8 e Lambda
Podemos tornar o código mais simples com o uso do Java 8 e Lambdas, desse modo acabaríamos com um código mais sucinto como este:
TransactionManager txManager = new TransactionManager();
txManager.doInTransaction((connection) -> {
// sua lógica de negócio vai aqui
});
Mas não se anime tanto, a maioria dos sistemas não usa Java 8. Para falar a verdade eles estão estagnados no Java 5 e 6, e sair destas versões é um desafio grande. Se brincar, o sistema que você começou a desenvolver em 2016 ainda usa Java 7, ou não?
Tenha controle sobre o código
Durante todo o artigo eu falei sobre "sistemas legados", mas a verdade é que é muito difícil e delicado definir o que este termo significa. Entre as inúmeras definições, eu gosto muito da definição do meu amigo Kico da iTexto, na qual ele fala:
Sistema legado é aquele cujo controle foi perdido por seus principais stakeholders. – Henrique Lobo Weissmann
Este controle na qual o Kiko fala abrange todo o contexto na qual o software está inserido, desde código, documentação até o cliente final. Mas se restringirmos este controle apenas ao código, já dar para ter idéia do quão grande é o desafio. Ter controle de uma base de código com mais de 10 anos não é uma tarefa fácil, principalmente se você está a frente da equipe, seja como líder técnico, arquiteto ou desenvolvedor sênior. Requer muita experiência e jogo de cintura, já que envolve desde a manutenção do código existente até problemas de infraestrutura, deploy e validação do que é entregue.
Não é por acaso que muitas consultorias propõe de cara a reescrita de um sistema legado, muitas vezes sem uma analise séria da base de código e do cenário na qual o sistema roda, afinal é muito mais fácil manter um sistema criado do zero. O problema, é que reescrever um sistema é muito caro e tende a não trazer nenhum benefício aparente a curto-médio prazo.
Ter controle sobre a base de código leva tempo e esforço, e certamente requer boas práticas de design e engenharia de software. Mas não se desespere, de inicio você vai perceber que há espaço para implementar diversas soluções que são bastante conhecidas no mercado, como controle de versão, build e deploy automatizados, APIs para envio de emails, controle threads, geração de relatórios etc. Não precisa ser algo grandioso, mas sim importante que te traga controle sobre aquela parcela de código legado.
Enfim, não se deixe desmotivar pela qualidade do código dos sistemas que você mantém, abrace o desafio e tente melhorar o sistema continuamente com o menor impacto possível. E você, como gerencia as transações na sua aplicação?
You might also be interested in these articles...
Desenvolvedor e instrutor na TriadWorks
Posted in: boas praticasfechar conexãojavajava eejdbcorientação a objetossistema legadotransaction managementtransactionaltransactions