Início › Desafios › Desafio 01
Somar 200 contas com uma única consulta
Este é o erro que mais aparece em code review de gente que está começando, e o que mais derruba automação em produção. O código passa no teste manual com três registros, sobe, e na primeira importação de dados a org devolve um erro que ninguém entende. A diferença entre o código que sobrevive e o que quebra costuma ser uma linha fora do lugar.
O cenário
A área comercial quer uma tela que mostre, para um grupo de contas selecionadas, quanto cada uma já fechou em oportunidades ganhas. O time recebeu essa demanda e entregou um método que percorre a lista de contas e, para cada uma, faz uma consulta buscando as oportunidades daquela conta.
Funcionou na apresentação, com três contas escolhidas na mão. Uma semana depois, a tela passou a ser usada em cima de uma lista de 200 contas e começou a estourar o limite de consultas da org. O motivo é simples: uma consulta por conta significa 200 consultas, e o Salesforce permite 100 por transação.
Sua missão é entregar a versão que aguenta qualquer volume.
O que você deve construir
Crie uma classe Apex chamada CarteiraAnalyzer com um método público e
estático, exatamente com esta assinatura:
public static Map<Id, Decimal> totalGanhoPorConta(List<Id> contaIds)
O método deve:
- Receber uma lista de Ids de
Account. - Somar o campo
AmountdasOpportunitydessas contas que estejam no estágioClosed Won. - Devolver um
Mapem que a chave é o Id da conta e o valor é o total somado.
Regras de aceite
São elas que definem se o desafio foi resolvido. Leia com atenção, porque duas delas são o coração do exercício:
- Exatamente uma consulta SOQL, não importa se a lista tem 1 ou 200 contas. Nenhuma consulta dentro de laço.
- Conta sem nenhuma oportunidade ganha precisa aparecer no resultado com valor zero.
Ela não pode simplesmente faltar no
Map, senão quem consome o método é obrigado a tratar ausência de chave. - Lista vazia ou nula devolve um
Mapvazio, sem erro e sem consultar nada. - O método não lança exceção em nenhum cenário previsto acima.
- Oportunidade com
Amountem branco não deve contaminar a soma.
O que este desafio avalia
- Se você tira a consulta de dentro do laço, que é a base da bulkificação.
- Se você conhece agregação em SOQL e sabe ler um
AggregateResult. - Se você pensa no contrato do método, e não só no caminho feliz.
- Se você trata entrada vazia antes de gastar uma consulta.
Travei. Me dá uma direção (sem entregar a resposta)
Três perguntas que destravam o desafio:
- Existe uma forma de pedir ao SOQL que ele já traga a soma agrupada por conta, em vez de trazer todas as oportunidades para você somar no Apex? Pesquise por "SOQL aggregate functions GROUP BY".
- Quando você usa função de agregação, o retorno não é mais uma lista de objetos normais. Pesquise por "Apex AggregateResult example" e veja como dar um apelido para a coluna somada.
- Se o resultado da consulta só traz contas que têm oportunidade, como garantir que as
outras também apareçam no
Mapcom zero? Pense em qual estrutura você monta antes de rodar a consulta.
Sobre passar a lista para a consulta: procure "SOQL bind variable IN clause List". O bind é o que faz a mesma consulta valer para 1 ou 200 ids.
Como testar
Abra o Execute Anonymous na sua org de desenvolvimento e rode o código abaixo. Ele pega cinco contas quaisquer da org e chama o seu método:
// Pega 5 contas quaisquer da org para servir de entrada
List<Id> ids = new List<Id>();
for (Account a : [SELECT Id FROM Account LIMIT 5]) {
ids.add(a.Id);
}
Map<Id, Decimal> resultado = CarteiraAnalyzer.totalGanhoPorConta(ids);
System.debug('Contas consultadas: ' + ids.size());
System.debug('Chaves no resultado: ' + resultado.size());
System.debug('Resultado: ' + resultado);
// Checagem das regras de aceite
System.assertEquals(ids.size(), resultado.size(), 'Toda conta da entrada deve estar no resultado');
System.assertEquals(0, CarteiraAnalyzer.totalGanhoPorConta(new List<Id>()).size(), 'Lista vazia deve voltar Map vazio');
System.assertEquals(0, CarteiraAnalyzer.totalGanhoPorConta(null).size(), 'Lista nula deve voltar Map vazio');
System.debug('Consultas usadas: ' + Limits.getQueries() + ' de ' + Limits.getLimitQueries());
Repare na última linha: ela mostra quantas consultas a transação gastou. É a forma mais direta de provar que você respeitou a regra principal. Se aparecer um número alto ali, tem consulta dentro de laço.
Validação opcional na sua org
Deixe um teste verificável, sem entregar seu código ao site
Depois de criar a CarteiraAnalyzer, adicione a classe de teste abaixo com o nome exato
WL_Desafio01Verifier e execute esse teste na Developer Org ou no Trailhead Playground.
O conector procura somente o nome da classe e o resultado da execução mais recente pela Tooling API.
O corpo da sua solução não é consultado nem armazenado.
@IsTest
private class WL_Desafio01Verifier {
@IsTest
static void validarDesafio() {
List<Account> contas = new List<Account>();
for (Integer i = 0; i < 200; i++) {
contas.add(new Account(Name = 'Conta ' + i));
}
insert contas;
insert new List<Opportunity>{
new Opportunity(Name='Ganha 100', AccountId=contas[0].Id,
StageName='Closed Won', CloseDate=Date.today(), Amount=100),
new Opportunity(Name='Ganha 50', AccountId=contas[0].Id,
StageName='Closed Won', CloseDate=Date.today(), Amount=50),
new Opportunity(Name='Aberta', AccountId=contas[0].Id,
StageName='Prospecting', CloseDate=Date.today(), Amount=900),
new Opportunity(Name='Sem valor', AccountId=contas[1].Id,
StageName='Prospecting', CloseDate=Date.today(), Amount=null)
};
List<Id> ids = new List<Id>();
for (Account conta : contas) ids.add(conta.Id);
Test.startTest();
Integer antes = Limits.getQueries();
Map<Id, Decimal> total = CarteiraAnalyzer.totalGanhoPorConta(ids);
Map<Id, Decimal> vazio = CarteiraAnalyzer.totalGanhoPorConta(new List<Id>());
Map<Id, Decimal> nulo = CarteiraAnalyzer.totalGanhoPorConta(null);
Integer usadas = Limits.getQueries() - antes;
Test.stopTest();
System.assertEquals(1, usadas, 'O desafio permite exatamente uma consulta');
System.assertEquals(200, total.size(), 'Toda conta precisa aparecer no Map');
System.assertEquals(150, total.get(contas[0].Id), 'Somente Closed Won entra na soma');
System.assertEquals(0, total.get(contas[1].Id), 'Amount nulo não contamina a soma');
System.assertEquals(0, total.get(contas[199].Id), 'Conta sem ganho continua no Map');
System.assertEquals(0, vazio.size(), 'Lista vazia devolve Map vazio');
System.assertEquals(0, nulo.size(), 'Lista nula devolve Map vazio');
}
}
- Crie a classe com o nome indicado e rode WL_Desafio01Verifier em Apex Test Execution.
- Volte para Meu progresso e conecte a mesma org.
- Use “Validar desafio 01”. A consulta é somente leitura e não executa nada na org.
Ver o tutorial guiado para conectar e validar →
Conexão segura
Resolveu? O ganho aqui não é o método em si: é sair com o reflexo de olhar todo laço e perguntar "o que acontece se isso rodar com 200 registros". Esse reflexo é o que separa código que passa em revisão de código que volta.