Construindo um Motor de ETL Flexível: Mapeamento Dinâmico de Planilhas com C++
Este artigo detalha a arquitetura que quebrou a "tirania do layout fixo", criando um sistema de mapeamento dinâmico com um dicionário de sinônimos para ler planilhas de forma inteligente e adaptável.
No nosso artigo anterior, contamos a história de como um processo crítico de carga de dados foi transformado, reduzindo seu tempo de execução de uma semana para apenas 15 minutos (leia também o artigo "e Uma Semana a 15 Minutos: Um Case Prático de Modernização de Processos de Carga de Dados"). A velocidade foi o resultado mais chamativo, mas a verdadeira revolução aconteceu em um nível mais profundo: na arquitetura do software. Hoje, vamos mergulhar no coração dessa mudança e desvendar como quebramos a "tirania do layout fixo".
O Problema: Acoplamento Forte e Código Quebradiço
Todo desenvolvedor que já trabalhou com importação de dados conhece o inimigo: o layout fixo. É o paradigma onde o código é escrito com a premissa de que os dados sempre chegarão em uma ordem e posição exatas. No nosso sistema antigo, o código essencialmente dizia: "Eu espero que o CPF esteja na primeira coluna, o nome na segunda, e o valor na terceira".
Isso cria o que chamamos de acoplamento forte: o código e o formato dos dados estão intrinsecamente ligados. Qualquer mudança no arquivo de entrada – uma coluna movida, um cabeçalho renomeado – quebrava a aplicação. A consequência era um ciclo vicioso de falhas, chamados de suporte e intervenções manuais de desenvolvedores para ajustar um código que nunca foi projetado para se adaptar.
A Nova Arquitetura: Mapeamento por Significado, não por Posição
A virada de chave foi mudar a pergunta fundamental que o software fazia. Em vez de perguntar "O que está na coluna 'A'?", passamos a perguntar "Onde está a informação do 'CPF'?". Essa mudança nos levou a uma arquitetura baseada em metadados, que funciona em duas etapas principais:
- Definição de "Variáveis de Negócio": Para cada tipo de processo (Renovação, Rendas, Faturamento, etc.), definimos internamente um conjunto fixo de variáveis que representam as informações de que o sistema precisa. Por exemplo, para um script de Renovação Cadastral, as variáveis são [CPF_CNPJ], [MOTIVO], [COOP] e [UA]. Essas variáveis são o "contrato" que o motor de script espera.
- Mapeamento Dinâmico ("De-Para"): Ao receber uma planilha, o primeiro passo da aplicação é ignorar os dados e ler apenas a linha de cabeçalho. Em seguida, ela tenta associar cada nome de coluna encontrado a uma de suas "variáveis de negócio" internas. A coluna chamada "Documento" na planilha do usuário, por exemplo, seria mapeada para a variável interna [CPF_CNPJ].
Com essa abordagem, a posição física da coluna se torna irrelevante. O que importa é o seu significado, que é estabelecido por essa camada de mapeamento.
Tornando o Mapeamento Inteligente: O Dicionário de Sinônimos
A pergunta seguinte foi: como fazer esse mapeamento de forma automática e eficiente? Sabíamos que os usuários não nomeavam suas colunas de maneira consistente. "CPF", "CPF/CNPJ", "Documento", "Nº do CPF" – todos poderiam se referir à mesma informação.
A solução foi criar o que chamo de "dicionário de sinônimos" no coração da aplicação. Em automato.cpp, implementamos a função DevolveIndex(), que é responsável por essa tradução. Ela recebe o nome de uma coluna e, através de uma série de verificações, retorna o código numérico da "variável de negócio" correspondente.
Veja um trecho simplificado da sua lógica:
Snippet de Código 1:
// Trecho da função DevolveIndex em automato.cpp
int DevolveIndex(AnsiString nome, int pos, int arq)
{
// Mapeamento para Tipo A (arq == 2)
if (arq == 2) {
if (nome == "CPF")
return 21; // Variável [CPF (Para o TIPO A)]
if (nome == "MOTIVO")
return 22; // Variável [MOTIVO (Para o TIPO A)]
if (nome == "NOME")
return 24; // Variável [NOME (Para o TIPO A)]
if (nome == "AGE")
return 25; // Variável [AGE (Para o TIPO A)]
}
// Mapeamento para Tipo B (arq == 3)
if (arq == 3) {
if (nome == "CNPJ" || nome == "CGC")
return 27; // Variável [CNPJ(PARA O TIPO B)]
if (nome == "MES" || nome == "MÊS")
return 30; // Variável [MES(PARA O TIPO B)]
if (nome == "ANO")
return 31; // Variável [ANO(PARA O TIPO B)]
}
// ... dezenas de outras checagens para todos os tipos de arquivo e colunas
return -1; // Código para "Não reconhecido"
}
Essa função, chamada durante o processo de verificação (verificar()), analisa cada coluna do arquivo Excel. Ao reconhecer "CPF" ou "CPF/CNPJ", ela internamente sabe que aquela coluna corresponde à variável 27. Com essa inteligência, a aplicação se tornou proativa, conseguindo mapear corretamente a grande maioria das planilhas sem qualquer intervenção humana.
Quando a Mágica não é Suficiente: A Intervenção Humana
E se um usuário nomear uma coluna como "Doc." e nosso dicionário não a reconhecer? O sistema antigo simplesmente falharia. O novo sistema, no entanto, entende que isso é uma oportunidade de aprender.
Quando DevolveIndex() retorna -1 (não reconhecido) para uma coluna obrigatória, o fluxo não é interrompido. Em vez disso, a aplicação abre uma tela de associação (TFRela). Nessa tela, o usuário vê a lista de colunas não reconhecidas e, para cada uma, pode selecionar a "variável de negócio" correta em uma lista suspensa.
Ao confirmar, o mapeamento é salvo e o processo continua. Na prática, o usuário "ensina" a aplicação sobre um novo layout de planilha, e essa configuração pode ser reutilizada em futuras importações. É a combinação perfeita entre automação e controle do usuário.
Conclusão: Construindo Sistemas que se Adaptam
A transição de um código rígido para uma arquitetura flexível foi a mudança mais significativa do projeto. Ao combinar um mapeamento automático via "dicionário de sinônimos" com uma interface para intervenção manual, criamos um sistema resiliente, que não quebra, mas se adapta.
O resultado mais profundo dessa abordagem foi a mudança de responsabilidade. A tarefa de lidar com novos layouts de planilha, que antes era um fardo para a equipe de desenvolvimento, tornou-se uma simples configuração realizada pelo próprio usuário. Isso liberou tempo de desenvolvimento, deu autonomia ao usuário e tornou todo o processo de carga de dados infinitamente mais robusto.
Agora que entendemos como o sistema se tornou flexível para entender os dados, a pergunta que fica é: como garantimos que o processamento desses 600.000 registros fosse feito em minutos? No próximo artigo, vamos fundo nas estratégias de I/O e nas otimizações em C++ que nos deram essa performance extrema (leia também o artigo "Otimização Extrema em C++: Acelerando 600.000 Registros").