Acionadores do Apex em massa
Objetivos de aprendizagem
Após concluir esta unidade, você estará apto a:
- Escrever acionadores que operem em coleções de sObjects.
- Escrever acionadores que executam operações eficientes de SOQL e DML.
Padrões de design dos acionadores em massa
Os acionadores do Apex são otimizados para funcionar em massa. Recomendamos usar padrões de design em massa para o processamento de registros em acionadores. Usando padrões de design em massa, seus acionadores têm melhor desempenho, desperdiçam menos recursos do servidor e são menos propensos a exceder os limites da plataforma.
O benefício do seu código em massa é que ele pode processar um grande número de registros de forma eficiente e ser executado dentro dos limites de administrador na Lightning Platform. Esses limites de administrador servem para garantir que o código sem controle não monopolize os recursos na plataforma multilocatária.
As seções a seguir demonstram as principais formas de processar em massa seu código do Apex nos acionadores: operando em todos os registros no acionador e realizando SOQL e DML em coleções de sObjects, em vez de sObjects individuais de cada vez. As melhores práticas recomendadas para SOQL e DML em massa aplicam-se a qualquer código do Apex, incluindo SOQL e DML em classes. Os exemplos dados são baseados em acionadores e usam a variável de contexto Trigger.new
.
Operando nos conjuntos de registros
Primeiramente, abordaremos o conceito de design em massa mais básico em acionadores. Os acionadores em massa operam em todos os sObjects no contexto do acionador. Normalmente, os acionadores funcionam em um registro caso a ação que disparou o acionador vier da interface de usuário. Mas se a origem da ação for DML em massa ou API, o acionador funciona em um conjunto de registros e não em um só registro. Por exemplo, quando você importa muitos registros pela API, os acionadores funcionam no conjunto de registros completo. Portanto, uma boa prática de programação é sempre assumir que o acionador funciona em uma coleção de registros para que ele funcione em todas as circunstâncias.
O acionador seguinte (MyTriggerNotBulk
) assume que apenas um registro fez o acionador disparar. Esse acionador não funciona em um conjunto de registros completo quando vários registros são inseridos na mesma transação. O próximo exemplo apresenta uma versão em massa.
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.new[0]; a.Description = 'New description'; }
Este exemplo (MyTriggerBulk
) é uma versão modificada do MyTriggerNotBulk
. Ele usa um loop for
para iterar sobre todos os sObjects disponíveis. Este loop funcionará se Trigger.new
tiver um ou muitos sObjects.
trigger MyTriggerBulk on Account(before insert) { for(Account a : Trigger.new) { a.Description = 'New description'; } }
Executando SOQL em massa
As consultas SOQL podem ser poderosas. É possível recuperar registros relacionados e verificar uma combinação de várias condições em uma única consulta. Usando recursos SOQL, você pode escrever menos códigos e fazer menos consultas ao banco de dados. Fazer menos consultas ao banco de dados ajuda a evitar extrapolar limites de consultas, que são 100 consultas SOQL para Apex síncrono ou 200 para Apex assíncrono.
O acionador a seguir (SoqlTriggerNotBulk
) mostra um padrão de consulta SOQL que deve ser evitado. O exemplo faz uma consulta SOQL dentro de um loop for
para obter as oportunidades relacionadas de cada conta, que são executadas uma vez para o sObject de cada conta em Trigger.new
. Se você tem uma grande lista de contas, uma consulta SOQL dentro de um loop for
pode resultar em muitas consultas SOQL. O próximo exemplo mostra a abordagem recomendada.
trigger SoqlTriggerNotBulk on Account(after update) { for(Account a : Trigger.new) { // Get child records for each account // Inefficient SOQL query as it runs once for each account! Opportunity[] opps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId=:a.Id]; // Do some other processing } }
O seguinte exemplo (SoqlTriggerBulk
) é uma versão modificada do anterior e mostra a melhor prática para a execução de consultas SOQL. A consulta SOQL faz o trabalho pesado e é chamada uma vez fora do loop principal.
- A consulta SOQL usa uma consulta interna – (
SELECT Id FROM Opportunities
) – para obter oportunidades relacionadas das contas. - A consulta SOQL está ligada aos registros de contexto do acionador usando a cláusula
IN
e vinculando a variávelTrigger.new
na cláusulaWHERE
—WHERE Id IN :Trigger.new
. Essa condiçãoWHERE
filtra as contas apenas para os registros que dispararam este acionador.
Combinar as duas partes na consulta resulta nos registros que queremos em uma chamada: as contas neste acionador com as oportunidades relacionadas de cada conta.
Após os registros e seus registros relacionados serem obtidos, o loop for
itera sobre os registros de interesse usando a variável de coleção, neste caso, acctsWithOpps
. A variável de coleção contém os resultados da consulta SOQL. Dessa forma, o loop for
itera somente sobre os registros que queremos operar. Como os registros relacionados já foram obtidos, não há a necessidade de mais consultas dentro do loop para obter os registros.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the accounts and their related opportunities. List<Account> acctsWithOpps = [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities) FROM Account WHERE Id IN :Trigger.new]; // Iterate over the returned accounts for(Account a : acctsWithOpps) { Opportunity[] relatedOpps = a.Opportunities; // Do some other processing } }
Como alternativa, se você não precisa dos registros da conta pai, pode obter apenas as oportunidades que estejam relacionadas às contas dentro deste contexto de acionador. Esta lista é especificada na cláusula WHERE
, combinando o campo AccountId
da oportunidade com a ID
das contas em Trigger.new
: WHERE AccountId IN :Trigger.new
. As oportunidades retornadas referem-se a todas as contas neste contexto de acionador e não a uma conta específica. Este próximo exemplo mostra a consulta usada para obter todas as oportunidades relacionadas.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Do some other processing } }
O exemplo anterior pode ser reduzido em tamanho, combinando a consulta SOQL com o loop for
em uma única instrução: o loop for
SOQL. Apresentamos outra versão deste acionador em massa que usa o loop for
SOQL.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger, // and iterate over those records. for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]) { // Do some other processing } }
Executando DML em massa
Ao executar chamadas DML em um acionador ou em uma classe, execute chamadas DML em uma coleção de sObjects, quando possível. Executar a DML em cada sObject individualmente faz uso dos recursos de forma ineficiente. O tempo de execução do Apex permite até 150 chamadas DML em uma transação.
Este acionador (DmlTriggerNotBulk
) executa uma chamada de atualização dentro de um loop for
que itera sobre oportunidades relacionadas. Se forem respeitadas determinadas condições, o acionador atualiza a descrição da oportunidade. Neste exemplo, a instrução de atualização é chamada, de forma ineficiente, uma vez para cada oportunidade. Se uma operação de atualização de conta em massa disparou o acionador, pode haver muitas contas. Se cada conta tem uma ou duas oportunidades, podemos facilmente terminar com mais de 150 oportunidades. O limite da instrução DML é de 150 chamadas.
trigger DmlTriggerNotBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; // Update once for each opportunity -- not efficient! update opp; } } }
O exemplo a seguir (DmlTriggerBulk
) mostra como realizar DML em massa de maneira eficiente com apenas uma chamada DML em uma lista de oportunidades. O exemplo adiciona o sObject Oportunidade que deseja atualizar a uma lista de oportunidades (oppsToUpdate
) do loop. Em seguida, o acionador executa a chamada DML fora do loop nesta lista, depois que todas as oportunidades tiverem sido adicionadas à lista. Este padrão usa apenas uma chamada DML independentemente do número de sObjects que está sendo atualizado.
trigger DmlTriggerBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; List<Opportunity> oppsToUpdate = new List<Opportunity>(); // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; oppsToUpdate.add(opp); } } // Perform DML on a collection update oppsToUpdate; }
Padrão de design em massa em ação: Exemplo de como obter registros relacionados
Aplicaremos os padrões de design que você aprendeu escrevendo um acionador que acessa oportunidades relacionadas das contas. Modifique o exemplo de acionador da unidade anterior para o acionador AddRelatedRecord
. O acionador AddRelatedRecord
funciona em massa, mas o acionador da unidade anterior não é tão eficiente quanto poderia ser porque ele itera sobre todos os registros sObject Trigger.New
. Este próximo exemplo modifica o código do acionador e a consulta SOQL para obter apenas os registros de interesse e, em seguida, iterar sobre esses registros. Se você não tiver criado o acionador, não se preocupe, pode criá-lo nesta seção.
Começaremos com a revisão dos requisitos para o acionador AddRelatedRecord
. O acionador dispara após a inserção ou atualização de contas. O acionador adiciona uma oportunidade padrão para cada conta que ainda não tem uma oportunidade.
Em primeiro lugar, contas recém-inseridas nunca têm uma oportunidade padrão, portanto, definitivamente precisamos adicionar uma. Porém, no caso de contas atualizadas, precisamos determinar se elas têm uma oportunidade relacionada ou não. Portanto, vamos separar como processamos inserções e atualizações usando uma instrução switch
na variável de contexto Trigger.operationType
. E, em seguida, manter o controle das contas que precisamos processar com uma variável toProcess
. Por exemplo:
List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // do stuff } when AFTER_UPDATE { // do stuff } }
Para todas as inserções de conta, simplesmente atribuímos as novas contas à lista toProcess
:
when AFTER_INSERT { toProcess = Trigger.New; }
Para atualizações, devemos descobrir quais contas existentes neste acionador ainda não têm uma oportunidade relacionada. Como este acionador é um pós-acionador, podemos consultar os registros afetados a partir do banco de dados. Veja a instrução SOQL, cujos resultados atribuímos à lista toProcess
.
when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; }
Agora usamos um loop for
para iterar pela lista toProcess
de contas e adicionamos uma oportunidade padrão relacionada a oppList
. Quando terminarmos, adicionamos em massa a lista de oportunidades usando a instrução DML insert
. Veja como criar ou atualizar o acionador completo.
- Se você já tiver criado o acionador
AddRelatedRecord
na unidade anterior, modifique-o, substituindo o conteúdo pelo acionador seguinte. Caso contrário, adicione o acionador seguinte usando o Developer Console e digiteAddRelatedRecord
como o nome do acionador.trigger AddRelatedRecord on Account(after insert, after update) { List<Opportunity> oppList = new List<Opportunity>(); // Add an opportunity for each account if it doesn't already have one. // Iterate over accounts that are in this trigger but that don't have opportunities. List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // All inserted Accounts will need the Opportunity, so there is no need to perform the query toProcess = Trigger.New; } when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; } } for (Account a : toProcess) { // Add a default opportunity for this account oppList.add(new Opportunity(Name=a.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=a.Id)); } if (oppList.size() > 0) { insert oppList; } }
- Para testar o acionador, crie uma conta na interface de usuário do Salesforce e dê a ela o nome
Lions & Cats
(Leões e Gatos). - Na lista relacionada de Oportunidades, na página da conta, localize a nova oportunidade
Lions & Cats
(Leões e Gatos). O acionador adicionou a oportunidade automaticamente!
Recursos