Engenharia de Dados: Estratégias para Isolamento de Ambientes em Banco Compartilhado

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.

Tags: java Spring MyBatis AOP database-isolation

Publicado em 10-15 01:58