Início › Desafios › Desafio 08 › Gabarito
Gabarito: salvar o lote parcial quando um registro falha
Esta página destrincha a solução do oitavo desafio. Se você ainda não tentou, vale abrir uma org, montar
um lote com uma conta sem nome no meio e ver o insert comum desfazer tudo. Sentir o tudo ou
nada na prática antes de trocá-lo ensina mais do que ler o gabarito de primeira.
Spoiler à frente. A solução completa está logo abaixo. Voltar para o desafio
O problema em uma frase
O insert comum do Apex é atômico: ou grava a lista inteira, ou não grava nada. Basta uma
linha reprovar numa validação para a plataforma lançar DmlException, com uma mensagem como
REQUIRED_FIELD_MISSING, Required fields are missing: [Name], e desfazer o lote todo. Numa
importação real, onde dado sujo é a regra e não a exceção, isso significa perder 199 registros bons por
causa de um torto. O desafio pede a versão que grava o que dá para gravar e presta contas do resto.
A virada de chave é esta: o Apex tem uma forma de DML que não é tudo ou nada.
Database.insert(lista, false) grava os válidos, deixa os inválidos de fora e, em vez de
lançar exceção, devolve um resultado por linha dizendo o que aconteceu. O trabalho passa a ser ler esse
resultado e traduzir cada falha em algo acionável.
A solução completa
public with sharing class ImportadorLote {
// Relatorio do import: os numeros e o detalhe de cada falha.
public class ResultadoImport {
public Integer totalRecebido;
public Integer gravados;
public Integer falhados;
public List<FalhaLinha> falhas;
public ResultadoImport() {
this.totalRecebido = 0;
this.gravados = 0;
this.falhados = 0;
this.falhas = new List<FalhaLinha>();
}
}
// Uma linha que falhou: onde estava, o que era e por que falhou.
public class FalhaLinha {
public Integer indice;
public String nome;
public String erro;
public FalhaLinha(Integer indice, String nome, String erro) {
this.indice = indice;
this.nome = nome;
this.erro = erro;
}
}
public static ResultadoImport importar(List<Account> contas) {
ResultadoImport r = new ResultadoImport();
// Entrada vazia ou nula: nada a gravar, relatorio zerado, sem DML.
if (contas == null || contas.isEmpty()) {
return r;
}
r.totalRecebido = contas.size();
// allOrNone = false: grava os validos e devolve um SaveResult por linha,
// na mesma ordem da entrada, em vez de abortar tudo no primeiro erro.
Database.SaveResult[] resultados = Database.insert(contas, false);
for (Integer i = 0; i < resultados.size(); i++) {
Database.SaveResult sr = resultados[i];
if (sr.isSuccess()) {
r.gravados++;
continue;
}
// Um registro pode acumular varios erros: junte todos.
List<String> mensagens = new List<String>();
for (Database.Error e : sr.getErrors()) {
mensagens.add(e.getStatusCode() + ': ' + e.getMessage());
}
r.falhados++;
r.falhas.add(new FalhaLinha(i, contas[i].Name, String.join(mensagens, ' | ')));
}
return r;
}
}
Destrinchando parte por parte
1. O relatório que presta contas, não só grava
public class ResultadoImport {
public Integer totalRecebido;
public Integer gravados;
public Integer falhados;
public List<FalhaLinha> falhas;
public ResultadoImport() {
this.totalRecebido = 0;
this.gravados = 0;
this.falhados = 0;
this.falhas = new List<FalhaLinha>();
}
}
Devolver uma classe de relatório, e não void, é a decisão de design que dá valor ao método.
Uma importação que só grava e cala não serve para nada quando algo falha: quem chamou precisa saber quanto
entrou, quanto ficou de fora e por quê. Os três contadores mais a lista de falhas transportam essa
resposta inteira.
Inicializar tudo no construtor (zeros e lista vazia) elimina uma classe de bug: o método retorna em mais
de um ponto, e em nenhum deles falhas será null. Quem consome nunca precisa
checar nulo antes de iterar, o que cumpre direto a regra da borda sobre entrada nula.
2. A guarda de entrada, antes de tocar no banco
if (contas == null || contas.isEmpty()) {
return r;
}
r.totalRecebido = contas.size();
A regra pede que lista nula ou vazia devolva zeros sem gravar nada. Tratar isso na primeira linha, antes
de qualquer DML, é mais barato e mais claro do que deixar o Database.insert lidar com lista
vazia. Como o ResultadoImport já nasce zerado, basta retornar o objeto como está.
Só depois da guarda o totalRecebido recebe o tamanho da lista. A partir daqui existe pelo
menos um registro para gravar, e o resto do método pode assumir isso.
3. O coração: allOrNone igual a false
Database.SaveResult[] resultados = Database.insert(contas, false);
Este é o trecho que resolve o desafio. O segundo parâmetro do Database.insert é o
allOrNone. Com true (o padrão, e o mesmo comportamento do insert
da DML), qualquer linha inválida derruba tudo. Com false, a plataforma grava as linhas
válidas, ignora as inválidas e devolve um Database.SaveResult por registro, na mesma ordem
da entrada.
O ponto sutil, e que a regra da borda cobra, é que nesse modo o método não lança
DmlException por causa de linha reprovada. Ele só marca aquele resultado como sem
sucesso. É por isso que um lote inteiramente inválido devolve relatório em vez de estourar: não há
exceção para tratar, há resultados para ler. Uma DML só, para o lote todo, também mata a tentação de
gravar registro a registro num laço, que estouraria o limite de DML em qualquer volume real.
4. Ler o resultado linha por linha
for (Integer i = 0; i < resultados.size(); i++) {
Database.SaveResult sr = resultados[i];
if (sr.isSuccess()) {
r.gravados++;
continue;
}
// ... trata a falha
}
O laço é por índice de propósito. A lista de SaveResult vem alinhada com a lista de entrada:
o resultado na posição i corresponde à conta na posição i. Percorrer com um
índice inteiro, em vez de um for-each, é o que permite recuperar tanto o veredito
(resultados[i]) quanto o registro original (contas[i]) sem manter um mapa
paralelo. É essa garantia de ordem que cumpre a regra da correlação de índice.
sr.isSuccess() separa os dois caminhos. Sucesso apenas incrementa o contador e segue com
continue. Todo o trabalho interessante mora no ramo da falha.
5. Extrair o erro real, com código e mensagem
List<String> mensagens = new List<String>();
for (Database.Error e : sr.getErrors()) {
mensagens.add(e.getStatusCode() + ': ' + e.getMessage());
}
r.falhados++;
r.falhas.add(new FalhaLinha(i, contas[i].Name, String.join(mensagens, ' | ')));
sr.getErrors() devolve uma lista, não um erro único, porque uma mesma linha pode reprovar em
vários pontos ao mesmo tempo: campo obrigatório faltando e uma regra de validação, por exemplo. Iterar
sobre todos e juntar com String.join preserva o quadro completo, em vez de reportar só o
primeiro problema e esconder o resto.
Cada Database.Error traz getStatusCode() (o código estável, como
REQUIRED_FIELD_MISSING, ótimo para o código reagir) e getMessage() (o texto
legível para o humano). Guardar os dois dá o melhor dos mundos. O FalhaLinha ainda registra o
índice e o Name do registro, então o relatório final aponta exatamente qual linha corrigir,
não um "algo deu errado" genérico.
Erros comuns
- Usar o
insertpuro dentro de umtry/catch. Capturar aDmlExceptionnão salva os válidos: o rollback já aconteceu antes de você tratar. O tudo ou nada é doinsert, não docatch. - Gravar em laço para isolar o erro. Fazer um
insertpor registro dentro de umforaté funciona com três contas, mas estoura o limite de 150 DML por transação no primeiro lote grande. OallOrNone = falseresolve com uma DML só. - Reportar mensagem genérica. Devolver "falha ao salvar" joga fora a informação que a
plataforma já deu de graça. Sem o
getStatusCodee ogetMessagereais, o usuário não sabe o que corrigir. - Ler só o primeiro erro da linha.
getErrors()é uma lista. Pegar sógetErrors()[0]esconde os demais problemas do mesmo registro e obriga o usuário a reimportar várias vezes, descobrindo um erro por vez. - Perder a correlação com a entrada. Sem o índice, o relatório diz "uma linha falhou"
sem dizer qual. Confiar na ordem do
SaveResulte guardar oié o que torna a falha acionável. - Deixar
falhaspoder ser nulo. Inicializar no construtor evita oNullPointerExceptionna camada que consome, principalmente no caminho de lista vazia.
Como saber se a sua solução passou
O script de teste do desafio cobre os casos que mais reprovam: os válidos gravam com uma inválida no meio, a falha aponta o índice certo com erro real, o lote todo inválido não lança e a entrada nula volta zerada. O critério objetivo é este:
List<Account> lote = new List<Account>{
new Account(Name = 'Valida'), new Account(), new Account(Name = 'Outra')
};
ImportadorLote.ResultadoImport r = ImportadorLote.importar(lote);
System.assertEquals(2, r.gravados, 'Os validos gravam');
System.assertEquals(1, r.falhados, 'So a invalida falha');
System.assertEquals(1, r.falhas[0].indice, 'A falha aponta a posicao certa');
System.assert(!String.isBlank(r.falhas[0].erro), 'A falha traz o erro real');
Se qualquer chamada estoura com DmlException em vez de devolver o relatório, o
allOrNone ainda está em true. Se gravados vier zero quando havia
linha válida, provavelmente o rollback do tudo ou nada ainda está em jogo. E se Limits.getDmlStatements()
passar de um por chamada, há gravação em laço escondida.
Um passo além
Para levar a rotina ao nível que se usa em produção, três evoluções:
-
Respeitar a segurança de quem chama. Existe a sobrecarga
Database.insert(lista, false, AccessLevel.USER_MODE), que aplica CRUD e FLS do usuário em cima do salvamento parcial. Numa importação exposta a usuário final isso deixa de ser opcional. O raciocínio completo está em segurança em Apex: CRUD, FLS e sharing. - Escalar para centenas de milhares. O salvamento parcial brilha ainda mais dentro de um Batch Apex, onde cada chunk grava o que é válido e acumula as falhas sem derrubar o job inteiro. Quando e como sair da transação síncrona está em Queueable, Batch e Schedulable: qual async usar.
- Não estourar os limites da transação. Um lote parcial ainda respeita o teto de linhas por DML e de registros processados. Entender esses limites antes de aumentar o tamanho do lote evita trocar um erro por outro. A base está em governor limits em Apex: o que são e como não bater.
O que fica deste desafio: gravar em lote e tratar erro não são fases separadas. O
allOrNone = false e o SaveResult transformam uma importação frágil em uma que
sobrevive a dado sujo e diz, linha por linha, o que corrigir. Esse reflexo separa quem escreve o caminho
feliz de quem entrega rotina de produção.