Início › Desafios › Desafio 08
Salvar o lote quando um registro inválido derruba tudo
A planilha do cliente tem 200 contas. Você mapeia, monta a lista e roda o insert. Uma linha
no meio veio sem nome, e a org devolve REQUIRED_FIELD_MISSING e desfaz o lote inteiro: as 199
contas boas não entraram por causa de uma. O usuário só vê "deu erro" e reclama que a importação não
funciona. O trabalho de quem sabe Apex de verdade não é impedir toda linha ruim, é não perder as
boas por causa dela e dizer, linha por linha, o que falhou. É isso que este desafio treina.
O cenário
A rotina de importação atual é tudo ou nada. Alguém escreveu o caminho feliz e nunca testou com dado sujo:
// A rotina de hoje: um registro ruim aborta os 200.
public static void importar(List<Account> contas) {
insert contas;
}
Enquanto a planilha vem limpa, funciona. No primeiro arquivo real, com um CNPJ faltando aqui e um nome em
branco ali, o insert lança DmlException, faz rollback de tudo e a importação
inteira volta ao zero. Pior: a exceção só cita o primeiro erro, então quem importou não sabe quantas
linhas estavam ruins nem quais.
Você decide trocar o tudo ou nada por um salvamento parcial. As contas válidas entram, as inválidas ficam de fora, e no fim a rotina devolve um relatório: quantas gravaram, quantas falharam e, para cada falha, em que posição da lista estava e qual foi o erro exato. Esse relatório é o que transforma "deu erro" em "as linhas 12, 47 e 130 estão sem nome, corrija e reimporte só elas".
O que você deve construir
Crie uma classe Apex chamada ImportadorLote com duas classes internas de
resultado e um método público e estático, exatamente com esta assinatura:
public class ResultadoImport {
public Integer totalRecebido;
public Integer gravados;
public Integer falhados;
public List<FalhaLinha> falhas;
}
public class FalhaLinha {
public Integer indice; // posicao do registro na lista original
public String nome; // identificacao do registro (Account.Name)
public String erro; // mensagem de erro consolidada
}
public static ResultadoImport importar(List<Account> contas)
O método deve:
- Receber a lista de
Accounta importar e gravar todas as que forem válidas numa única operação de DML. - Não interromper a gravação por causa de um registro inválido no meio do lote.
- Devolver um
ResultadoImportcom a contagem de gravados e falhados e, para cada linha que falhou, o índice na lista original, o nome e a mensagem de erro real da plataforma.
Regras de aceite
São elas que definem se o desafio foi resolvido. As duas últimas são o coração do exercício, porque é onde quase todo mundo esquece o caso da borda:
- Os válidos gravam mesmo com inválidos no lote. Um único registro ruim não pode
derrubar os demais. Isso elimina de saída o
insertpuro, que é tudo ou nada. - Uma DML para o lote inteiro. Nada de gravar registro por registro dentro de um laço. A operação de gravação acontece uma vez só, não importa se são 3 ou 200 contas.
- O erro vem da plataforma, não de você. Para cada linha que falhou, colete a mensagem real devolvida pelo resultado da gravação, não um texto genérico do tipo "falha ao salvar". Se um registro acumular mais de um erro, consolide todos.
- As contagens fecham.
gravados + falhadostem que ser igual atotalRecebido, e cada falha precisa apontar o índice correto do registro na lista original. O resultado da gravação vem na mesma ordem da entrada: use isso. - Borda: lote todo inválido não lança exceção. Uma lista em que todos os registros
falham devolve o relatório com
gravados = 0e a lista completa de falhas, semDmlExceptione semnull. - Borda: lista nula ou vazia devolve zeros. Sem consultar, sem gravar nada e sem erro,
com
falhascomo lista vazia, nuncanull.
O que este desafio avalia
- Se você conhece a forma de DML que grava o que dá para gravar em vez de abortar no primeiro erro.
- Se você sabe ler o resultado de uma gravação parcial e extrair o erro de cada linha, com código e mensagem.
- Se você mantém a correlação entre a entrada e o resultado sem gambiarra, usando a ordem que a plataforma garante.
- Se você pensa no contrato do método diante de lote todo inválido, lista vazia e entrada nula, não só no caminho feliz.
Travei. Me dá uma direção (sem entregar a resposta)
Quatro perguntas que destravam o desafio:
- Existe uma forma de DML que aceita um parâmetro dizendo "não é tudo ou nada"? Pesquise por "Salesforce Database.insert allOrNone false" e veja o que ela devolve em vez de lançar exceção.
- O retorno dessa operação é uma lista. O que cada item dela sabe sobre a linha correspondente? Procure "Apex Database.SaveResult isSuccess getErrors".
- Cada erro tem um código e uma mensagem legível. Como extrair os dois? Veja "Apex Database.Error getStatusCode getMessage" e lembre que uma linha pode ter vários erros.
- Como saber a qual registro da entrada cada resultado corresponde? Pense na ordem: a lista de resultados vem alinhada com a lista de entrada. Um laço por índice resolve a correlação sem precisar de mapa.
Sobre a borda: com allOrNone igual a false, a operação não lança exceção
por causa de linha inválida, ela só marca aquele resultado como sem sucesso. Isso é justamente o que faz
o lote todo inválido devolver relatório em vez de estourar.
Como testar
Abra o Execute Anonymous na sua org de desenvolvimento e rode o código abaixo. Ele monta um lote com duas contas válidas e uma inválida no meio (sem nome, que é campo obrigatório):
// Lote com validas e uma invalida no meio (Name em branco reprova no insert).
List<Account> lote = new List<Account>{
new Account(Name = 'Cliente Valido A'),
new Account(),
new Account(Name = 'Cliente Valido B')
};
ImportadorLote.ResultadoImport r = ImportadorLote.importar(lote);
// 1. Os validos gravaram mesmo com a invalida no meio, e as contagens fecham.
System.assertEquals(3, r.totalRecebido, 'Deve contar tudo que recebeu');
System.assertEquals(2, r.gravados, 'As duas validas tem que gravar');
System.assertEquals(1, r.falhados, 'So a invalida falha');
// 2. A falha aponta o indice certo e traz o erro real da plataforma.
System.assertEquals(1, r.falhas[0].indice, 'A invalida estava na posicao 1');
System.assert(!String.isBlank(r.falhas[0].erro), 'A falha tem que trazer o erro real');
System.debug('Erro da linha 1: ' + r.falhas[0].erro);
// 3. Uma unica DML para o lote inteiro: prova que nao gravou em laco.
System.debug('DML usados ate aqui: ' + Limits.getDmlStatements() + ' de ' + Limits.getLimitDmlStatements());
// 4. Borda: lote todo invalido nao lanca e devolve o relatorio.
List<Account> soInvalidas = new List<Account>{ new Account(), new Account() };
ImportadorLote.ResultadoImport ri = ImportadorLote.importar(soInvalidas);
System.assertEquals(0, ri.gravados, 'Nenhuma grava');
System.assertEquals(2, ri.falhados, 'As duas falham, sem excecao');
// 5. Borda: entrada nula devolve zeros, sem DML e sem erro.
ImportadorLote.ResultadoImport rn = ImportadorLote.importar(null);
System.assertEquals(0, rn.totalRecebido, 'Nulo nao grava nada');
System.assert(rn.falhas.isEmpty(), 'Nulo nao tem falhas');
Se o teste 3 mostrar mais de uma DML, você provavelmente está gravando dentro de um laço: junte tudo numa
chamada só. E se a primeira asserção falhar com uma exceção em vez de um valor errado, é sinal de que o
insert ainda está em modo tudo ou nada.
Resolveu? O ganho aqui não é a classe: é entender que gravar em lote e tratar erro não são etapas separadas. Quem domina o salvamento parcial entrega importações que sobrevivem a dado sujo e dizem exatamente o que corrigir, em vez de morrer no primeiro registro torto.