InícioDesafios › Desafio 01

Desafio 01 Nível: Fundamento

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.

SOQL agregado Bulkificação GROUP BY AggregateResult Map em Apex Governor limits

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 Amount das Opportunity dessas contas que estejam no estágio Closed Won.
  • Devolver um Map em 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 Map vazio, sem erro e sem consultar nada.
  • O método não lança exceção em nenhum cenário previsto acima.
  • Oportunidade com Amount em 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 Map com 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.

Gabarito comentado

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

Ver o gabarito

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.