Início › Desafios › Desafio 01 › Gabarito
Gabarito: somar 200 contas com uma única consulta
Esta página destrincha a solução do primeiro desafio. Se você ainda não tentou resolver, vale voltar e passar pelo menos meia hora tentando: ler o gabarito antes de tentar é a forma mais rápida de achar que entendeu sem ter aprendido. Se já tentou, vamos comparar sua solução com esta.
Spoiler à frente. A solução completa está logo abaixo. Voltar para o desafio
O problema em uma frase
O código original fazia uma consulta por conta. Com 200 contas na entrada, isso significa 200 consultas
numa transação que só permite 100. O erro que aparece na org é
System.LimitException: Too many SOQL queries: 101, e ele não acusa a linha culpada de forma
óbvia: acusa a linha que estourou a conta, que pode estar bem longe da causa.
Toda a solução gira em torno de uma ideia: a quantidade de consultas não pode depender da quantidade de registros. Uma consulta resolve 1 conta ou 200 da mesma forma.
A solução completa
public with sharing class CarteiraAnalyzer {
public static Map<Id, Decimal> totalGanhoPorConta(List<Id> contaIds) {
Map<Id, Decimal> totais = new Map<Id, Decimal>();
// Entrada vazia ou nula sai antes de gastar consulta.
if (contaIds == null || contaIds.isEmpty()) {
return totais;
}
// Toda conta pedida comeca em zero. E isto que garante que a conta
// sem oportunidade ganha continue aparecendo no resultado.
for (Id contaId : contaIds) {
totais.put(contaId, 0);
}
// Uma consulta agregada resolve a lista inteira. O bind :contaIds
// vale para 1 ou 200 ids sem mudar nada no codigo.
List<AggregateResult> linhas = [
SELECT AccountId conta, SUM(Amount) total
FROM Opportunity
WHERE AccountId IN :contaIds
AND StageName = 'Closed Won'
AND Amount != null
GROUP BY AccountId
];
// O AggregateResult devolve os valores pelo apelido definido acima.
for (AggregateResult linha : linhas) {
Id conta = (Id) linha.get('conta');
Decimal total = (Decimal) linha.get('total');
totais.put(conta, total == null ? 0 : total);
}
return totais;
}
}
Destrinchando parte por parte
1. A saída antecipada
if (contaIds == null || contaIds.isEmpty()) {
return totais;
}
Parece detalhe, mas tem duas razões concretas. A primeira é que uma consulta com
IN :listaVazia é válida em Apex e não quebra, mas gasta uma das suas 100 consultas para
devolver nada. Se este método for chamado dentro de um laço em outro lugar do sistema (coisa comum), essa
consulta inútil vira o gargalo.
A segunda é o null. Se alguém chamar o método com null e você não tratar, o
for logo abaixo lança NullPointerException, e a regra de aceite pedia
explicitamente que o método não lançasse exceção.
2. O laço que preenche zero (a parte que a maioria esquece)
for (Id contaId : contaIds) {
totais.put(contaId, 0);
}
Esta é a regra de aceite que mais reprova soluções. O raciocínio natural é: consulto, agrupo, devolvo. O problema é que a consulta agregada só devolve linha para conta que tem oportunidade ganha. Conta sem nenhuma simplesmente não aparece no resultado.
Se você devolver só o que veio da consulta, quem consumir o seu método recebe um Map em que
algumas chaves existem e outras não. A tela que usa isso vai mostrar célula em branco em vez de
R$ 0,00, ou pior, vai quebrar ao tentar formatar um valor nulo. Você empurrou o problema
para o próximo desenvolvedor.
Pré-preencher com zero resolve isso na origem: o contrato do método passa a ser "toda conta que você me passar volta no resultado", e quem consome pode confiar.
3. A consulta agregada
List<AggregateResult> linhas = [
SELECT AccountId conta, SUM(Amount) total
FROM Opportunity
WHERE AccountId IN :contaIds
AND StageName = 'Closed Won'
AND Amount != null
GROUP BY AccountId
];
Aqui moram três decisões:
- Somar no banco, não no Apex. Existe uma solução alternativa que traz todas as oportunidades e soma num laço. Ela também usa uma consulta só, então passa na regra principal. Mas se uma conta tiver 50 mil oportunidades, você traz 50 mil registros para a memória e vai esbarrar no limite de heap. Deixar o banco somar é mais barato em memória e mais rápido.
-
O bind
:contaIds. É o que torna a consulta indiferente ao volume. Também é o que evita concatenar ids numa string, prática que além de frágil abre porta para injection quando o dado vem de fora (tema do desafio seguinte). -
O apelido
contaetotal. Sem apelido, o Salesforce nomeia a coluna agregada comoexpr0. Funciona, mas é ilegível e quebra silenciosamente se alguém acrescentar outra função de agregação antes dela.
4. Lendo o AggregateResult
for (AggregateResult linha : linhas) {
Id conta = (Id) linha.get('conta');
Decimal total = (Decimal) linha.get('total');
totais.put(conta, total == null ? 0 : total);
}
AggregateResult não é um sObject comum: você não acessa linha.AccountId, e sim
linha.get('apelido'), que devolve Object. Por isso os casts explícitos.
O put aqui sobrescreve o zero que colocamos antes, para as contas que de fato têm valor. As
que não têm permanecem com zero. É a combinação dos passos 2 e 4 que satisfaz a regra de aceite.
Erros comuns
- Consulta dentro do laço. O erro original do cenário. Funciona no teste com poucos registros e derruba a produção na primeira operação em massa.
- Devolver só as contas com resultado. Passa no teste superficial, falha na segunda regra de aceite e cria bug na camada de cima.
- Trazer todas as oportunidades e somar em Apex. Aceitável, mas escala pior. Se você fez assim, sua solução não está errada: está menos eficiente. Vale refazer com agregação para praticar.
- Esquecer o filtro de estágio. Somar tudo em vez de só
Closed Wonmuda o significado do número e ninguém percebe até alguém questionar o relatório. - Concatenar os ids na cláusula IN. Frágil, feio e inseguro quando a entrada vem de fora.
Como saber se a sua solução passou
O script de teste do desafio termina imprimindo o consumo de consultas da transação. É a prova objetiva:
System.debug('Consultas usadas: ' + Limits.getQueries() + ' de ' + Limits.getLimitQueries());
Com a solução acima, esse número fica em 1 (mais a consulta que o próprio script faz para buscar as contas, então você verá 2). Se aparecer um número que cresce junto com a quantidade de contas, tem consulta dentro de laço em algum lugar.
Um passo além
Se quiser levar sua solução para o nível que se espera em produção, três evoluções:
-
Respeitar as permissões de quem chamou. Acrescente
WITH USER_MODEna consulta. Sem isso o Apex roda com acesso total e pode devolver soma de oportunidade que o usuário nem deveria enxergar. O raciocínio completo está em segurança em Apex: CRUD, FLS e sharing. -
Escrever a classe de teste de verdade. O script de Execute Anonymous serve para
conferir na hora, mas não vai para o repositório. Um teste com
@TestSetupcriando as contas e oportunidades, incluindo o caso da conta sem oportunidade, é o que garante que a regra continue valendo quando outra pessoa mexer no código. -
Tornar o estágio configurável.
'Closed Won'fixo no código funciona até alguém renomear o estágio na org. Custom Metadata resolve isso sem deploy, e é o tema do artigo sobre KPIs configuráveis sem hardcode.
O que fica deste desafio: o reflexo de olhar qualquer laço e perguntar "o que acontece se isso rodar com 200 registros". Em entrevista técnica de Salesforce essa é, com folga, a pergunta mais provável sobre Apex.