InícioDesafios › Desafio 08

Desafio 08 Nível: Intermediário

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.

Database.insert allOrNone Database.SaveResult Salvamento parcial Tratamento de erro Correlação de índice

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 Account a 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 ResultadoImport com 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 insert puro, 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 + falhados tem que ser igual a totalRecebido, 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 = 0 e a lista completa de falhas, sem DmlException e sem null.
  • Borda: lista nula ou vazia devolve zeros. Sem consultar, sem gravar nada e sem erro, com falhas como lista vazia, nunca null.

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.

Gabarito comentado

A solucao completa, destrinchada parte por parte: o porque de cada decisao, os erros mais comuns e como levar a rotina para o nivel de producao. Tente resolver antes de abrir.

Ver o gabarito

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.