Estratégia Inicial de Isolamento
A solução padrão envolve adicionar um campo ambiente_id em todas as tabelas relevantes. O objetivo é garantir que operações em pré-produção não afetem os dados da produção. Embora funcionalmente simples, gerenciar isso manualmente em milhares de entidades e mappers torna-se inviável devido ao alto custo de manutenção e propensão a erros humanos.
A otimização adequada utiliza um interceptador do framework ORM. Com o uso de bibliotecas como o JSqlParser, é possível manipular a sintaxe SQL dinâmica antes da execução, injetando as cláusulas de filtro sem exigir alterações no código de negócio das entidades ou dos arquivos XML de mapeamento.
A lógica básica envolve alterar a instrução SELECT e INSERT para incluir uma condição adicional:
SELECT colunas FROM tabela_x
WHERE ambiente_id IN ('producao', 'todas')
AND condicao_original;
Para transições históricas ou compatibilidade, novos registros recebem o identificador do ambiente atual, enquanto dados antigos podem ser marcados como 'todas' para persistir acessibilidade entre ambientes durante a migração.
O Desafio Operacional
Com a evolução do sistema, surgiram necessidades específicas que exigiam flexibilidade sobre esse isolamento rígido. Cenários típicos incluem:
- Sincronização de dados entre serviços externos que requerem visibilidade em todos os ambientes.
- Facilitar testes de regressão onde correções precisam ser validadas contra dados reais da produção simulados.
- Depuração remota de falhas operacionais.
A abordagem inicial para atender esses requisitos consistiu na inserção manual de blocos de código no nível da camada de serviço. Programadores alteravam variáveis de contexto via ThreadLocal antes e após chamadas críticas de API.
Código Não Recomendado (Anti-Padrão)
Embora funcione, essa prática espalha a lógica de controle por todo o código fonte, violando princípios de separação de preocupações:
// Armazenar estado anterior
String priorAmbiente = ContextUser.getUser().getPrioridadeEnv();
// Forçar alteração para todos os ambientes
ContextUser.getUser().setPrioridadeEnv(ConfigGlobal.AMBIENTES_TODOS);
// Execução da lógica de negócio
servico.processaPedido();
// Restaurar estado original
ContextUser.getUser().setPrioridadeEnv(priorAmbiente);
Dessa forma, três linhas de código devem aparecer repetidamente em diferentes pontos do fluxo, aumentando o risco de esquecimento na restauração do estado ou de vazamento de contexto entre threads assíncronas.
Refatoração com AOP e Anotações
Uma solução robusta centraliza essa regra utilizando Orientação a Objetos Assistiva (AOP) e metadados via anotações. O objetivo é definir explicitamente quais métodos devem ignorar as regras de filtragem, mantendo o corpo do método limpo.
A primeira etapa é criar uma anotização personalizada para identificar os pontos de corte:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnorarFiltroAmbiental {
// Listas opcionais de ambientes para ignorar especificamente
String[] ambientesExclusao() default {};
// Lista de tabelas específicas que seriam afetadas
String[] tabelasAlvo() default {};
}
Em seguida, desenvolve-se um Aspect que detecta quando um método marcado é invocado. Esse aspecto define o valor no escopo ThreadLocal necessário para o interceptador do banco de dados:
@Aspect
@Component
public class AspectoDeFiltroAmbiental {
private static final ThreadLocal<boolean> skipEnv = new ThreadLocal<>();
@Around("@annotation(ignorarFiltroAmbiental)")
public Object around(ProceedingJoinPoint joinPoint, IgnorarFiltroAmbiental ignore) throws Throwable {
boolean anteriorEstado = Boolean.TRUE.equals(skipEnv.get());
try {
skipEnv.set(true); // Ativa modo "sem filtro"
return joinPoint.proceed();
} finally {
skipEnv.set(anteriorEstado); // Restaura estado seguro
}
}
}</boolean>
O Interceptador do MyBatis deve ser modificado para consultar este estado antes de aplicar a transformação SQL. Se o sinalizador estiver ativo, a cláusula WHERE de filtragem é suprimida:
@Component
public class FiltroAmbienteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
if (isIgnorado()) {
// Retorna diretamente a consulta original sem modificação
return invocation.proceed();
}
// Aplica lógica de parse e adição de WHERE
String sqlAtualizada = processarSql(mappedStatement.getSqlCommand());
// ... lógica de substituição de SQL
return invocation.proceed();
}
private boolean isIgnorado() {
return Boolean.TRUE.equals(SkipEnvHelper.getStatus());
}
}
Esta estrutura permite que apenas o cabeçalho (Controller ou Serviço público) seja anotado. O resto do fluxo herda o comportamento corretamente dentro do mesmo contexto de thread.
Análise Comparativa
| Característica | Injeção Manual de ThreadLocal | Implementação com AOP + Anotação |
|---|---|---|
| Mantenabilidade | Baixa (Código repetitivo) | Alta (Lógica centralizada) |
| Risco de Vazamento | Altíssimo | Controlado |
| Clareza de Intenção | Fica escondida no fluxo | Explícita no contrato do método |
| Extensibilidade | Precisa editar多处 | Adicionar nova regra no Aspet |
É crucial considerar a integridade dos dados. Ao permitir acessos globais, verifica-se se a auditoria está habilitada. Em sistemas críticos, o acesso a ambientes compartilhados geralmente exige aprovações administrativas, independentemente da técnica utilizada.
Conclusão Técnica
Padrões como injeção de filtros em nível de banco de dados oferecem escalabilidade inicial, mas enfrentam desafios complexos na gestão de exceções. Utilizar recursos de meta-programação como Aspect-Oriented Programming (AOP) ajuda a manter a regra de domínio segregada do código de fluxo.
Desenvolvedores sêniores devem evitar soluções "patchwork" que resolvem problemas pontuais à custa da legibilidade global. Sempre que uma lógica for necessária mais de uma vez, ela pertence ao framwork ou aos utilitários centrais, nunca ao ciclo de vida da aplicação específica.