Trigger Apex in blocco
Obiettivi di apprendimento
Schemi di progettazione in blocco dei trigger
Il vantaggio dell'utilizzo in blocco del codice è che il codice utilizzato in blocco è in grado di elaborare un gran numero di record in modo efficiente e di essere eseguito entro i limiti del governor su Lightning Platform. I limiti del governor servono a garantire che l'esecuzione di codice priva di controllo non monopolizzi le risorse della piattaforma multitenant.
Nelle sezioni che seguono vengono illustrati i principali metodi per utilizzare in blocco il codice Apex nei trigger: operare su tutti i record presenti nel trigger ed eseguire SOQL e DML sulle raccolte di sObject anziché su un singolo sObject per volta. Le best practice di utilizzo in blocco per SOQL e DML si applicano a qualsiasi codice Apex, compresi SOQL e DML nelle classi. Gli esempi riportati si basano sui trigger e utilizzano la variabile di contesto Trigger.new
.
Operare su insiemi di record
Vediamo innanzitutto il concetto di progettazione in blocco dei trigger più elementare. I trigger utilizzati in blocco operano su tutti gli sObject nel contesto del trigger. Di norma, i trigger operano su un solo record se l'azione che ha innescato il trigger proviene dall'interfaccia utente. Ma se l'origine dell'azione è un'operazione DML in blocco o l'API, il trigger opera su un insieme di record anziché su un singolo record. Ad esempio, quando si importano molti record tramite l'API, i trigger operano sull'intero insieme di record. Pertanto, una buona prassi di programmazione consiste nel presumere sempre che il trigger operi su un insieme di record, in modo che funzioni in ogni circostanza.
Il trigger seguente (MyTriggerNotBulk
) presuppone che l'attivazione del trigger sia stata causata da un solo record. Questo trigger non funziona su un insieme di record completo quando vengono inseriti più record nella stessa transazione. Nel prossimo esempio viene mostrata una versione modificata per l'utilizzo in blocco.
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.new[0]; a.Description = 'New description'; }
Questo esempio (MyTriggerBulk
) è una versione modificata di MyTriggerNotBulk
. Utilizza un loop for
per eseguire l'iterazione su tutti gli sObject disponibili. Questo loop funziona sia se Trigger.new
contiene un solo sObject sia se ne contiene di più.
trigger MyTriggerBulk on Account(before insert) { for(Account a : Trigger.new) { a.Description = 'New description'; } }
Eseguire SOQL in blocco
Le query SOQL possono essere molto efficaci. È possibile recuperare record correlati e verificare una combinazione di più condizioni in un'unica query. Utilizzando le funzioni SOQL, è possibile scrivere una minore quantità di codice ed effettuare un minor numero di query sul database. Effettuare un minor numero di query sul database permette di evitare di raggiungere i limiti relativi alle query, che sono 100 query SOQL per Apex sincrono o 200 per Apex asincrono.
Il trigger seguente (SoqlTriggerNotBulk
) mostra un modello di query SOQL da evitare. Nell'esempio viene effettua una query SOQL all'interno di un loop for
per acquisire le opportunità correlate per ciascun account, che viene eseguita una sola volta per ciascun sObject Account in Trigger.new
. Se l'elenco degli account è molto lungo, una query SOQL all'interno di un loop for
potrebbe generare un numero eccessivo di query SOQL. Nel prossimo esempio viene mostrato l'approccio consigliato.
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 } }
Il seguente esempio (SoqlTriggerBulk
) è una versione modificata di quello precedente e mostra una best practice per l'esecuzione di query SOQL. La query SOQL fa il lavoro pesante e viene chiamata una sola volta al di fuori del loop principale.
- La query SOQL utilizza una query interna – (
SELECT Id FROM Opportunities
) – per ottenere le opportunità correlate agli account. - La query SOQL è collegata ai record del contesto del trigger mediante la clausola
IN
e vincolando la variabileTrigger.new
nella clausolaWHERE
:WHERE Id IN :Trigger.new
. Questa condizioneWHERE
filtra gli account includendo solo i record che hanno attivato quel trigger.
Combinando le due parti della query si ottengono i record desiderati in un'unica chiamata: gli account di questo trigger con le relative opportunità di ciascun account.
Dopo aver ottenuto i record e i relativi record, il loop for
esegue un'iterazione sui record di interesse utilizzando la variabile raccolta (in questo caso), acctsWithOpps
. La variabile raccolta contiene i risultati della query SOQL. In questo modo, il loop for
esegue l'iterazione solo sui record su cui desideriamo operare. Poiché i record correlati sono già stati ottenuti, non sono necessarie altre query all'interno del loop per ottenere quei record.
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 } }
In alternativa, se non sono necessari i record Account controllanti, è possibile recuperare solo le opportunità correlate agli account all'interno di questo contesto trigger. Questo elenco viene specificato nella clausola WHERE
mediante la corrispondenza del campo AccountId
dell'opportunità con l'ID
degli account in Trigger.new
: WHERE AccountId IN :Trigger.new
. Le opportunità restituite sono correlate a tutti gli account del contesto di questo trigger e non a un account specifico. Nel prossimo esempio viene mostrata la query utilizzata per ottenere tutte le opportunità correlate.
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 } }
È possibile ridurre le dimensioni dell'esempio precedente combinando la query SOQL e il loop for
in un'unica istruzione: il loop for
SOQL. Ecco un'altra versione di questo trigger in blocco che utilizza un 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 } }
Eseguire DML in blocco
Quando si eseguono chiamate DML in un trigger o in una classe, è bene che queste siano eseguite su una raccolta di sObject ove possibile. L'esecuzione di DML su ciascun oggetto sObject comporta un utilizzo inefficiente delle risorse. Il runtime Apex consente fino a 150 chiamate DML in una singola transazione.
Questo trigger (DmlTriggerNotBulk
) effettua una chiamata ad update all'interno di un loop for
che esegue l'iterazione delle opportunità correlate. Se si verificano determinate condizioni, il trigger aggiorna la descrizione dell'opportunità. In questo esempio, l'istruzione update viene chiamata, in modo inefficiente, una volta per ogni opportunità. Se un'operazione di aggiornamento in blocco di account attiva il trigger, gli account interessati possono essere molti. Se ciascun account contiene una o due opportunità, è facile ritrovarsi con più di 150 opportunità. Il limite delle istruzioni DML è di 150 chiamate.
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; } } }
Nel prossimo esempio (DmlTriggerBulk
) viene mostrato come eseguire DML in blocco in modo efficiente con una sola chiamata DML su un elenco di opportunità. Nell'esempio l'sObject Opportunity (Opportunità) da aggiornare viene aggiunto a un elenco di opportunità (oppsToUpdate
) nel loop. Successivamente, il trigger esegue la chiamata DML su questo elenco all'esterno del loop, dopo che tutte le opportunità sono state aggiunte all'elenco. Questo schema utilizza una sola chiamata DML a prescindere dal numero di sObject che vengono aggiornati.
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; }
Schemi di progettazione in blocco in azione: esempio di acquisizione di record correlati
Applichiamo gli schemi di progettazione che hai appreso, scrivendo un trigger che acceda alle opportunità correlate degli account. Modifica l'esempio di trigger AddRelatedRecord
dell'unità precedente. Il trigger AddRelatedRecord
opera in blocco, ma quello dell'unità precedente non è efficiente come potrebbe essere perché esegue un'iterazione su tutti i record sObject presenti in Trigger.New
. Nel prossimo esempio vengono modificati sia il codice del trigger sia la query SOQL in modo da ottenere solo i record di interesse ed eseguire l'iterazione su questi ultimi. Se non hai ancora creato questo trigger, non preoccuparti: puoi crearlo in questa sezione.
Iniziamo con un esame dei requisiti per il trigger AddRelatedRecord
. L'esecuzione del trigger viene innescata dopo l'inserimento o l'aggiornamento dei conti. Il trigger aggiunge un'opportunità predefinita per ogni account che non dispone già di un'opportunità.
Innanzitutto, gli account appena inseriti non hanno mai un'opportunità predefinita, quindi dobbiamo decisamente aggiungerne una. Ma per gli account aggiornati, occorre determinare se dispongono di un'opportunità correlata oppure no. Separiamo quindi le modalità di elaborazione degli inserimenti e degli aggiornamenti utilizzando un'istruzione switch
sulla variabile di contesto Trigger.operationType
. E poi teniamo traccia degli account che dobbiamo elaborare con una variabile toProcess
. Ad esempio:
List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // do stuff } when AFTER_UPDATE { // do stuff } }
Per tutti gli account inseriti, assegniamo semplicemente i nuovi account all'elenco toProcess
:
when AFTER_INSERT { toProcess = Trigger.New; }
Per gli aggiornamenti, dobbiamo capire quali account esistenti in questo trigger non dispongono già di un'opportunità correlata. Poiché il trigger è un trigger after, siamo in grado di eseguire una query sui record interessati nel database. Ecco l'istruzione SOQL, i cui risultati vengono assegnati all'elenco 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)]; }
Ora utilizziamo un loop for
per scorrere l'elenco degli account toProcess
e aggiungere un'opportunità correlata predefinita a oppList
. Quando abbiamo finito, aggiungiamo in blocco l'elenco delle opportunità utilizzando l'istruzione DML insert
. Ecco come creare o aggiornare il trigger completo.
- Se hai già creato il trigger
AddRelatedRecord
nell'unità precedente, modificalo sostituendone il contenuto con il seguente trigger. Diversamente, aggiungi il seguente trigger utilizzando la Developer Console e inserisciAddRelatedRecord
come nome del trigger.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; } }
- Per testare il trigger, crea un account nell'interfaccia utente Salesforce e chiamalo
Lions & Cats
(Leoni e gatti). - Nell'elenco correlato Opportunità della pagina dell'account, trova la nuova opportunità
Lions & Cats
(Leoni e gatti). Il trigger ha aggiunto l'opportunità automaticamente!